One of this site’s own pages carries a sentence that is true and stops exactly one clause short. It is in Hardening a Public-Facing Service, in the step about updates:

Then handle the part people skip: a patched library does not protect a process that is still running the old one. Install needrestart so you find out.

The missing clause is and needrestart is the thing that does not tell you. Here is a machine that has just taken a real OpenSSL security upgrade, with a live process holding the deleted library, running the tool exactly as the package’s own APT hook runs it:

Setting up libssl3t64:amd64 (3.0.13-0ubuntu3.15) ...

No services need to be restarted.
No containers need to be restarted.
No user sessions are running outdated binaries.

It had found five processes running deleted code. It printed none of them, because the report it produces is a list of names — service names, package names — and it had not managed to name any of them. One verbosity level up, everything it found is on the screen.

This page is about the half of patching that starts once the file on disk is already correct. Getting the file correct is The Life of a Package‘s subject and it is better on it than this page would be. What happens next — which processes picked the fix up, which ones nothing will ever restart, and how you prove any of it to somebody who does not believe you — has no page anywhere, and it is where the time goes.

Every number here came off one machine, and it is named: Ubuntu 24.04.4 with kernel 6.18.44, apt 2.8.3, dpkg 1.22.6, unattended-upgrades 2.9.1, needrestart 3.6-7ubuntu4.5, dnf 4.14.0. Where something could not be run here — anything RPM-side, anything Livepatch — the page says so and gives you no number.

What this page does not cover, and where it went instead.

  • How the machine learns an update exists, and what it checks before installing it — repositories, signatures, solvers, maintainer scripts, the package database. The Life of a Package owns all eight stages of it, and this page begins where that one ends.
  • Sandboxing a service so a compromise is worth lessHardening a Public-Facing Service. That page and this one are the two halves of the same afternoon; it assumes the patch worked, and this one is about finding out.
  • What a container image is and why yours is not patched by the hostContainers, All the Way Down. This page owns one narrow consequence: a containerised process running vulnerable code shows no sign of it in the place every tool looks.

And the honest limitation, stated here rather than at the end: the mechanism is Debian- and Ubuntu-shaped. The RPM side asks a different question with different code — that gets its own section rather than a column in a table — and everything about Livepatch here is Canonical’s own documentation rather than something measured.

The seven stages, from an advisory to a machine you can vouch for. Every section below is named after one.

  1. The version number that was designed not to move.
  2. What “security” means to your machine — which is not what it means to you.
  3. The automation that is already running, and what it is already not doing.
  4. The process still running the old code — and the tool that found it and said nothing.
  5. What a restart restarts, and the code on the box that no package owns.
  6. The reboot, and what livepatching actually removes.
  7. Proving it — to a scanner, an auditor, or yourself at three in the morning.

The diagnostic hinge: the kernel knows which processes are running deleted code. It does not know what to call them. Every restart tool is a guess at the name — and the ones it cannot name, it does not mention. So do not ask the tool what needs restarting. Ask it what it found, and then ask it what it is willing to say:

sudo needrestart -v -b 2>&1 | grep -E 'uses (deleted|obsolete)|^NEEDRESTART-'

Two lists come back. The uses deleted lines are what the kernel told it. The NEEDRESTART-SVC and NEEDRESTART-PID lines are what it will act on. Read the difference between them:

  • A uses deleted line with a matching SVC or PID line. The tool has it, and will restart it or already has. Stage 5.
  • A uses deleted line with no matching SVC or PID line. It found the process and dropped it: no package owns the binary, no unit owns the process, or the unit is on the tool’s own do-not-restart list. Nothing will ever restart this. You will. Stage 4 — and this branch is the reason the page exists.
  • No uses deleted lines, and the installed version is the current one. The process never mapped the file: interpreted code, a data file read once at startup, or a container running the image’s own copy. Check readlink /proc/PID/root before you believe the path. Stage 5.
  • No uses deleted lines, and the installed version is not the security version. The patch never landed at all. Stages 2–3.
  • NEEDRESTART-KSTA: 0. Not “no reboot needed” — “I could not find your kernel”. The reboot question is unanswered. Stage 6.

What the hinge gives up, said out loud. It is shaped for Debian and Ubuntu. The RPM tools ask a time-based question rather than a deletion-based one, so the two lists do not line up the same way — stage 4 gives you the parallel command and is explicit that the test underneath it is a different test.

Stage 1: The version number that was designed not to move

Distributions do not ship you the upstream fix. They take the upstream patch, apply it to the version they already shipped, and change as little else as possible — because the version you are running is the version the rest of your software was tested against. That is backporting, and it means the number your software prints is deliberately uninformative.

$ openssl version
OpenSSL 3.0.13 30 Jan 2024

$ zcat /usr/share/doc/libssl3t64/changelog.Debian.gz | grep -c '^openssl (3\.0\.13-'
15

Fifteen separate Ubuntu uploads of upstream 3.0.13, and all fifteen print that same line. A machine patched this morning and a machine last patched in January 2024 are indistinguishable by the command everyone runs. The only field that moves is one you have to ask for:

$ openssl version -a | grep 'built on'
built on: Tue Aug 18 12:10:36 2026 UTC

This is the fact that makes the rest of the page necessary, and it has a consequence people meet before they understand it: any scanner keyed to upstream version strings calls a fully patched distribution machine vulnerable. The canonical example is on this very box — needrestart 3.6, where upstream fixed CVE-2024-48990 in 3.8, and the changelog says the fix has been here all along:

needrestart (3.6-7ubuntu4.3) noble-security; urgency=medium
  * SECURITY UPDATE: incorrect usage of PYTHONPATH environment variable
    - debian/patches/CVE-2024-48990.patch: chdir to a clean directory
    - CVE-2024-48990

Proving that mechanically is stage 7’s job, because the obvious way of doing it is broken on precisely the package you would check first. Hold the fact for now: the number is not evidence, and it was never meant to be.

Two things about CVE numbers, and then this page will stop talking about them.

The CVSS score is not the number your distribution used. Ubuntu, Debian and Red Hat each assign their own priority to a vulnerability, and it is that priority — not the CVE’s score — that decides whether a fix is backported, how quickly, and on Ubuntu whether Livepatch will carry it.

And a CVE with no score at all is now normal. NIST changed NVD policy on 15 April 2026: enrichment is risk-prioritised rather than universal, so entries outside the priority criteria are marked as not scheduled for enrichment and older ones are not being revisited. Anything that matches vulnerabilities by NVD’s CPE strings — which is most scanners — is now matching against a deliberately incomplete set. An unscored CVE is not a backlog artefact any more.

Stage 2: What “security” means to your machine

Every guide on earth, this site’s included, says to restrict automatic updates to security updates. The advice is sound and almost nobody knows what the machine does with it. “Security” is not a property of a fix. It is an Origin, Suite and Label triple in a repository’s Release file, matched as a string. A package is a security update if it came from a pocket whose name matches, and for no other reason.

On Ubuntu that pocket is a copy, not a superset, and the arithmetic of it explains something almost every reader has seen. Measured across all 184 upgradable packages on this machine:

upgradable total:                            184
candidate in BOTH -security and -updates:    162
candidate in -security only:                   0
candidate in -updates only:                   22

Split those 22 by whether any newer version exists in -security at all, and the whole thing falls out:

A) nothing newer in -security  -> never installed automatically:   9
   procps, libproc2-0, libapparmor1, libinput10, libxmlb2,
   xserver-common, xvfb, libpfm4, libinput-bin

B) a LOWER version exists in -security -> that one is installed,
   and the package stays "upgradable" forever:                    13
   libkrb5-3        installed …2.6   security …2.7   updates …2.8
   python3.12       installed …0.13  security …0.15  updates …0.16
   linux-libc-dev   installed 6.8.0-111  security 6.8.0-138  updates 6.8.0-139

This is why apt list --upgradable never reaches zero on a machine that patches itself every night. Thirteen packages are being held one upload behind on purpose, nine more will never be offered, and the count is correct. It is a permanent, benign number that describes the policy working as designed.

Acting on that count is the mistake, and it is the most dangerous single change in this subject. Adding -updates to the allowed origins to make the number go to zero converts a machine that installs security fixes unattended into a machine that installs everything unattended, overnight, with no one watching. The alert clears. The risk moves.

One more trap in the same file, because it is silent: Debian and Ubuntu use different keys. Ubuntu ships Unattended-Upgrade::Allowed-Origins matching on ${distro_codename}-security; Debian ships Unattended-Upgrade::Origins-Pattern matching on codename= and label=. An Allowed-Origins block pasted onto a Debian machine is not an error and is not applied. And an Origins-Pattern block without #clear at the top is added to the stock list rather than replacing it.

Verify: apt-config dump | grep -iE 'Unattended-Upgrade::(Allowed|Origins)' prints what your machine is actually matching on, after every file in /etc/apt/apt.conf.d/ has had its say. That is the only place the answer exists.

Stage 3: The automation that is already running

On an Ubuntu cloud image, the first two commands in most hardening guides — including this site’s own — are no-ops. unattended-upgrades is a hard dependency of the base image, so it is already installed; the debconf default is already true, so it is already enabled; and dpkg-reconfigure --priority=low unattended-upgrades asks one boolean question and configures nothing else. The reader believes they turned something on. They confirmed a default.

The RPM side ships as the exact mirror image, and this one is not benign:

# /etc/dnf/automatic.conf, as shipped
upgrade_type = default      # everything, not just security
apply_updates = no          # download only; install nothing, ever

unattended-upgrades ships security-only and enabled; dnf-automatic ships everything-but-installs-nothing and disabled. Enable the timer without touching the file and the machine downloads packages nightly and installs none of them, exiting 0 every time. Flip only apply_updates and you have enabled unattended installation of the entire distribution. Both keys, or neither.

There is also a live bug worth knowing if you rely on the security filter there: dnf5 issue #2754, reported against 5.4.2.1 on Fedora 44 and closed via PR #2759, records that dnf5 upgrade --security did not install kernel security updates even though dnf5 advisory list --security listed them. A machine running dnf-automatic with upgrade_type = security was not getting kernel fixes. (Read from the issue tracker; not reproduced here.)

The question worth asking is not which tool is better. It is what is going to run on your machine tonight, and there is a command for that rather than a table:

systemctl list-timers --all | grep -Ei 'apt-daily|unattended|dnf-automatic'
apt-config dump | grep -iE 'APT::Periodic|Unattended-Upgrade::Automatic'

One detail from that second command surprises people: APT::Periodic::Unattended-Upgrade "1" is not a boolean. It is an interval in days. "7" is legal and means weekly; "always" is legal too. A machine set to "0" has it switched off, which looks identical to a machine that never configured it.

Verify: the two commands above should show a timer with a NEXT time in the future and a non-zero period. Either one alone proves nothing — a timer that fires nightly into a configuration that installs nothing is the failure that survives longest in this subject, and it is invisible from either half.

Stage 4: The process still running the old code

A package upgrade replaces a file. It does not touch the copy already mapped into a running process — that process keeps the old inode, deleted from the directory but very much alive, until it execs or exits. Everyone knows this. What almost nobody has looked at is what the tool that is supposed to tell you does with the knowledge.

It finds them. Then, if it cannot work out a name to report them under, it discards them silently. Here is the full path, from its own debug output, for one process holding a deleted library:

[main] #2715 uses deleted /home/claude/work/src2/libvuln.so.1
[main] #2715 running /etc/needrestart/hook.d/10-dpkg
dpkg-query: no path found matching pattern .../holder
[main] #2715 running /etc/needrestart/hook.d/10-dpkg no perfect hit found 0 pids -1
[main] #2715 running /etc/needrestart/hook.d/20-rpm ... no perfect hit
[main] #2715 running /etc/needrestart/hook.d/90-none ... no perfect hit

Three hooks tried, three failures, and then nothing: no NEEDRESTART-PID line, no NEEDRESTART-SVC line, and at the verbosity the APT hook uses, “No services need to be restarted.” The evidence exists at exactly one place in the source and is gated exactly onceprint STDERR "…#$pid uses deleted $path\n" if($nrconf{verbosity} > 1); — which is why the hinge at the top of this page is a verbosity flag and a grep.

On a real systemd host it does better than that trace suggests: it reads /proc/PID/cgroup, extracts a .service name and falls back to systemctl status, so a systemd-managed unpackaged binary is found. Three things still are not, and they are not exotic:

  • Anything whose cgroup read fails is skipped outrightnext unless (defined($unit)); — with no fallback to the hooks that would have tried.
  • Anything in a user session — a tmux pane, a screen, a stray nohup — is diverted into “user sessions are running outdated binaries”, a heading that reports and never restarts.
  • Anything in a container’s cgroup is handed to a container backend, and on this machine every one of those backends failed to load at all.

Three more things it does that are worth knowing before you trust it

It will not restart the daemons you would most want restarted. The shipped configuration carries a deny-list of about forty patterns, and it is a roll-call of exactly the long-lived processes that map OpenSSL and glibc:

$nrconf{override_rc} = {
    qr(^dbus) => 0,          qr(^network) => 0,   qr(^NetworkManager) => 0,
    qr(^wpa_supplicant) => 0, qr(^openvpn) => 0,  qr(^docker) => 0,
    qr(^libvirt) => 0,       qr(^systemd-logind) => 0,
    qr(^getty@.+\.service) => 0,  qr(^user@\d+\.service) => 0,
    ...
};

Declining to restart dbus automatically is a defensible decision. Declining without saying so, in a tool whose entire output is a statement about what needs restarting, is the same defect as the first one wearing a different hat.

Its support for interpreted languages is an mtime check on one file. A running Python process whose imported module has been patched underneath it produces nothing at all — no deleted mapping, no open handle, no report — because the tool looks at the entry script’s modification time and nothing else. Touch the entry script and it fires immediately. So it is false-negative on every imported module, and false-positive on any rsync without --times or any restore from backup.

And on Ubuntu it restarts services automatically by default. The APT hook runs it with -m u, which sets restart mode to automatic; every apt transaction since 22.04 has been restarting services on your behalf, minus the deny-list. That is worth knowing in both directions — it is why patching often does work without you, and it is why an upgrade occasionally bounces something you were using.

The Red Hat tool asks a different question

Not a different implementation of the same test — a different test. dnf needs-restarting compares the install time of the package owning each mapped file against the process’s start time. Deletion does not come into it; RPM renames the old file with a hexadecimal suffix before unlinking, so a stale mapping there reads /usr/lib64/libssl.so.3;68b7f2a1 (deleted) — a path that never existed as anything anyone typed.

The time-based test is in one way better: it catches a file replaced in place, with no unlink and no deleted marker, which the Debian-side tools cannot see at all. And it inherits the identical blind spot by completely different code — if pkg is None: continue. Both families drop every mapped file that no package owns, and neither says how many it dropped.

sudo dnf needs-restarting        # processes started before their packages
sudo dnf needs-restarting -s     # the same, grouped by systemd service

Verify: run the hinge command and count. If the number of uses deleted lines is larger than the number of NEEDRESTART-SVC and NEEDRESTART-PID lines, the difference is a list of processes nothing on the machine intends to restart, and you have just found the work.

Stage 5: What a restart restarts

systemctl restart replaces the processes in a unit’s cgroup. That is all it promises, and four common things are not covered by it.

  • Anything a supervisor other than systemd started. The unit restarts; the supervisor’s children are the supervisor’s business.
  • Anything running in a container. Restarting the container restarts the process against the image’s copy of the library, which the host’s patch never touched. Containers, All the Way Down has the mechanism; the operational half is that a rebuild is the patch and a restart is not.
  • Anything holding data rather than code — a CA bundle, a timezone table, a config parsed once at startup. No mapping, no deleted marker, nothing for any tool to find, and a service that will keep trusting a revoked certificate until something makes it read the file again.
  • Your own shell. The one you are typing in mapped libc when you logged in. It is on the list, and it is the process people are most surprised by.

Behind all four sits a larger question, and it has a command. How much of the code running on this machine does the package manager not know about?

# every mapped file on this box that no package owns
sudo awk '$6 ~ /^\// {print $6}' /proc/[0-9]*/maps | sort -u |
  while read f; do dpkg -S "$f" >/dev/null 2>&1 || echo "$f"; done

That list is the part of the machine apt cannot patch, however diligent your automation is: the vendor binary in /opt, anything installed by pip, npm, cargo or go install, Snap and Flatpak payloads, container layers. Most people are surprised by its length, and the surprise is the point — every entry on it is something that will never appear in apt list --upgradable no matter how long you wait.

Verify: after restarting what you believe you needed to, run the hinge command again. It should print no uses deleted lines for the paths you acted on. If a line survives a restart, the thing you restarted was not the thing holding the file.

Stage 6: The reboot, and what livepatching removes

Nothing happens in this stage, which is why it is short. What matters is that the two standard ways of asking whether you need one are both weaker than they look.

/var/run/reboot-required is written by maintainer scripts, through a helper that ships in update-notifier-common — and each caller guards on the helper being present. That package is absent from Ubuntu cloud images and from Debian entirely, so on a great many servers the file is never created and its absence means nothing at all. Where it does exist, /var/run/reboot-required.pkgs names which packages asked, which is the more useful file and the one nobody mentions.

dnf needs-restarting -r is ten package names. Not a mechanism — a hard-coded list, compared against boot time:

NEED_REBOOT = ['kernel', 'kernel-core', 'kernel-rt', 'glibc',
               'linux-firmware', 'systemd', 'dbus', 'dbus-broker',
               'dbus-daemon', 'microcode_ctl']

openssl is not on it. Neither is openssh, zlib, libxml2, sudo or libcurl. Exit 0 is a true statement about ten packages and silent about the other four thousand. The check that always works is comparing the running kernel with the newest installed one: uname -r against /boot/vmlinuz-*.

Which brings up livepatching, and the honest framing of it is not the one in the marketing. Livepatch is a filter, and it has four teeth. Canonical’s own documentation gives three of them: patches are provided “exclusively for Canonical-released kernels”; only for issues that have been assigned a CVE; and only for those “rated as high or critical priority”. Bug fixes that are not security issues, performance work, driver updates and new features are excluded by name. The fourth is in the supported-kernels page — patches are created for a kernel “for up to 9–13 months from its release date”, after which they stop, silently. Asked directly whether the service removes the need to reboot, the same documentation answers: “That is not the case.”

And it interacts with the previous point in a way worth stating plainly. Ubuntu’s kernel hook exits early when Livepatch reports that no upgrade is required, so the reboot flag is not written on a livepatched machine. Livepatching does not remove the reboot. It removes the notification of the reboot.

Verify: canonical-livepatch status --verbose on Ubuntu, or kpatch list on the Red Hat side, tells you whether anything is actually being livepatched and which kernel it is patching. (Neither could be run on the machine these numbers came from; both are the vendor’s documented interface.)

Stage 7: Proving it

The machine is fixed. Now somebody asks you to demonstrate it — a scanner, an auditor, a customer’s questionnaire, or you at three in the morning wondering whether the thing you read about applies here. This is where the page that opened with “the version number lies” has to close.

The answer is local, offline, and in the changelog: a distribution that backports a fix records the CVE in the package’s changelog.Debian.gz. The obvious command for that is broken on the most security-relevant package on the machine:

$ zgrep -c CVE-2026-28387 /usr/share/doc/openssl/changelog.Debian.gz
gzip: /usr/share/doc/openssl/changelog.Debian.gz: No such file or directory
0

Zero hits and exit 0, from a dangling symlink. It points at ../libssl3/changelog.Debian.gz; the binary package was renamed libssl3libssl3t64 in the 64-bit time_t transition, and the old docdir was left behind empty. Every “check the changelog” recipe on the internet returns a clean, confident nothing here.

The working form resolves the file to its binary package, then to its source package, then looks in the docdir of any installed binary from that source:

cve=$1; file=$2
bin=$(dpkg -S "$(readlink -f "$file")" | cut -d: -f1)
src=$(dpkg-query -W -f='${source:Package}\n' "$bin")
for p in $(dpkg-query -W -f='${binary:Package} ${source:Package}\n' |
           awk -v s="$src" '$2==s {print $1}'); do
  cl=/usr/share/doc/$p/changelog.Debian.gz
  [ -s "$cl" ] && zgrep -q "$cve" "$cl" && { echo "FIXED — named in $cl"; exit 0; }
done
echo "NOT named — which is not the same as vulnerable"

Run against the two examples from stage 1, that returns FIXED for OpenSSL and for needrestart — the latter being the whole backporting story in one line, since the installed version is 3.6 and upstream fixed the issue in 3.8.

And the limit, which matters as much as the method: the changelog proves the positive only. “Not named” means “this changelog does not say so”. It never means “you are vulnerable” — the fix may predate the packaging, the CVE may not apply to this branch, or the entry may be worded differently. A tool that reports “NOT FIXED” from this check is making a claim the evidence cannot support, which is the same error as everything else on this page, pointed the other way.

Two things to say to a scanner, then. Its version match is wrong by design — stage 1 — and the changelog is the counter-evidence. And “zero pending updates” is not a state your machine can reach — stage 2 — so an audit item written that way can only ever be failed.

A worked diagnosis: the patch count that would not go to zero

A small production host. Ubuntu 24.04, one Go application in /opt/app/bin/server behind nginx, started by a supervisor that is not systemd. unattended-upgrades is installed and enabled, as it is on every Ubuntu cloud image, and nobody has configured it.

Every obvious observable says nothing is wrong.

$ sudo tail -2 /var/log/unattended-upgrades/unattended-upgrades.log
2026-09-01 06:41:12 INFO Packages that will be upgraded: libssl3t64 openssl
2026-09-01 06:41:31 INFO All upgrades installed

$ ls /var/run/reboot-required
ls: cannot access '/var/run/reboot-required': No such file or directory

$ sudo needrestart -b
NEEDRESTART-VER: 3.6
NEEDRESTART-KCUR: 6.8.0-138-generic
NEEDRESTART-KSTA: 1

$ systemctl --failed
0 loaded units listed.

Nightly success in the log, no reboot flag, a clean restart check, no failed units. openssl version prints what it printed last year. The only thing that looks odd is a monitoring check that has been alerting for four months: apt list --upgradable never reaches zero. It sits at 22 and does not move.

Someone reasons that unattended-upgrades must be broken, adds -updates to the allowed origins, and the count goes to zero the next morning. The alert clears. Three weeks later the box is compromised through a library that was patched two months earlier.

The hinge routes it in one command.

$ sudo needrestart -v -b 2>&1 | grep -E 'uses (deleted|obsolete)|^NEEDRESTART-'
NEEDRESTART-VER: 3.6
[main] #1183 uses deleted /usr/lib/x86_64-linux-gnu/libssl.so.3
[main] #1184 uses deleted /usr/lib/x86_64-linux-gnu/libssl.so.3
NEEDRESTART-KCUR: 6.8.0-138-generic
NEEDRESTART-KSTA: 1

Two uses deleted lines. No SVC line, no PID line. That is the hinge’s second branch — it found them and dropped them — and two commands say why:

$ sudo dpkg -S "$(readlink -f /proc/1183/exe)"
dpkg-query: no path found matching pattern /opt/app/bin/server

$ cat /proc/1183/cgroup
0::/system.slice/supervisor.service

The binary is not dpkg-owned, so the package hook returns nothing; the cgroup names the supervisor rather than this process, so there is no unit to restart. On the Red Hat side the same process is dropped by if pkg is None: continue.

The fix is not the obvious one. Restarting the supervisor is correct and you should do it, but it is the symptom. The actual defect is that the count was right and the log was wrong. Those 22 pending updates were stage 2’s permanent skew — thirteen packages held one upload behind, nine never offered — a benign number that cannot be cleared. Meanwhile All upgrades installed had been printed nightly for four months, and it is emitted from if len(to_upgrade) == 0:. It is a statement about an empty set, not about the machine.

So the change that made the machine less safe was made to silence an alert on the one number that was telling the truth, while the number that was lying went unquestioned — because it was phrased as a sentence rather than a count.

And note where the hinge points: past the cause, which is what makes it worth having. It does not say “your binary is unpackaged”. It says the tool found something and did not mention it. The unpackaged Go binary is one instance; fix it and the shape recurs the next time anybody deploys something that is not a .deb. The same gap explains the container that shows nothing because there is nothing to find, the Python process whose imported module was patched, and the kernel check that answers 0 for “I could not tell”. One silence, four causes.

The moral, and it is the page in one line: a tool that reports by name is silent about everything it cannot name — and on a healthy-looking machine that silence is indistinguishable from good news.

Symptoms, and which stage owns them

What you seeStageWhat to run, or what it is
apt list --upgradable never reaches zero on a machine that patches nightly2correct and permanent; do not “fix” it by adding -updates
A scanner calls a fully patched machine vulnerable1, 7it is reading the upstream version; answer with the changelog
openssl version identical on two machines patched a year apart1openssl version -a | grep 'built on'
Automatic updates enabled, and nothing is ever installed3dnf-automatic ships apply_updates = no
“No services need to be restarted” on a machine that just took a security upgrade4the hinge command; count the two lists
A service still running old code after a successful restart5a supervisor, a container image, or a data file read at startup
A restart tool that reports nothing for a patched Python module4it checks the entry script’s mtime and nothing else
An upgrade bounced a service you were using4Ubuntu’s APT hook restarts automatically by default
dbus, docker or NetworkManager never restarted by anything4they are on the shipped deny-list, and it does not say so
/var/run/reboot-required absent on a machine that needs a reboot6no update-notifier-common; compare uname -r with /boot
dnf needs-restarting -r exits 0 after an OpenSSL update6true, and about ten packages that do not include openssl
NEEDRESTART-KSTA: 06“I could not find your kernel”, not “no reboot needed”
Code running that never appears in apt list --upgradable5the unowned-mappings command; expect a long list

Advice on this subject that has expired

Commonly saidWhat is actually true
apt install unattended-upgrades && dpkg-reconfigure turns it on”On an Ubuntu cloud image both are no-ops — it is a hard dependency and already enabled. The reconfigure asks one boolean.
systemctl enable --now dnf-automatic.timer turns on automatic updates”The shipped default is apply_updates = no. It downloads and installs nothing, exiting 0 nightly.
“Restrict it to security updates rather than everything”On Ubuntu it already is, by default — and that restriction is why Kerberos, AppArmor and the system Python sit one upload behind.
ls /var/run/reboot-required tells you a reboot is pending”Written by update-notifier-common, absent from Ubuntu cloud images and Debian. Also suppressed on a livepatched machine.
dnf needs-restarting -r tells you whether a reboot is required”A hard-coded list of ten package names, compared to boot time. openssl is not one of them.
needrestart restarts what needs restarting”It ships a deny-list of about forty patterns covering dbus, docker, NetworkManager, openvpn and systemd-logind, and declines silently.
unattended-upgrades --dry-run shows what will happen”It cannot list what it will skip, and says so in its own log: the list of kept packages can’t be calculated in dry-run mode.
/var/log/unattended-upgrades/ proves it has been running”All upgrades installed is logged when there was nothing to install — including under --dry-run.
“Livepatch means you do not need to reboot”Canonical’s own answer is “That is not the case.” High and critical CVEs only, Canonical kernels only, and a 9–13 month window per kernel.
“Every CVE has a CVSS score my scanner can match on”NIST stopped universal enrichment on 15 April 2026. Unscored is now the normal state.
lsof | grep DEL finds everything running old code”Misses every containerised process — which has a clean mapping of a path the host has already patched.
“A container inherits the host’s patches”Measured: a process in another mount namespace shows a clean, undeleted mapping and runs the old code.

How to tell whether a page about patching is worth reading

Everything above comes from one model, and naming it is more useful than the list. Every page on this subject writes about packages, and every reader’s problem is about processes — so the corpus treats a statement about the package database as though it were a statement about the running machine. Watch it generate the rest:

  1. “Install unattended-upgrades” is a statement about the package database. It says nothing about whether any process picked the fix up — which is the entire missing second half of the subject.
  2. apt list --upgradable should be empty” is the package database again, and on Ubuntu it is a number that cannot reach zero — so the advice generates the one change that makes a machine less safe.
  3. ls /var/run/reboot-required reads a file written by maintainer scripts, about packages. It is not a fact about the kernel that is running, and it is missing on the machines most people have.
  4. A restart tool dropping a process it has already found, because its last step converts a fact about a process into the name of a package or a unit, and refuses to report what it cannot name. That is the hinge.
  5. A scanner matching upstream version strings — a package fact, applied to a machine where the package fact was deliberately made uninformative.
  6. “Restrict it to security updates” — a statement about an Origin string in a Release file, presented as a statement about vulnerability.

One error, six symptoms. It survives because the happy path never contradicts it: on a machine where every service is a packaged daemon with a systemd unit and nothing runs in a container, “the package database is the machine” is true — and that is the machine tutorials are written on.

The one-question test. Ask what the page says to do about a process that is running patched code and is not a systemd service. If every remedy it offers is a package name, a config file or systemctl restart, it was written about the package database and not about a running machine.

What the silences prove

  • No mention of what happens to a process that is not a systemd service — the author has only ever patched machines where every service is a distro package. They have never run an application.
  • No mention of the difference between -security and -updates — they have never watched the pending count fail to reach zero for months. That absence dates the author’s experience rather than their knowledge.
  • needrestart recommended with no caveat — they have never run it at -v. Everything wrong with it is printed one verbosity level up.
  • The word “backport” with no command attached — they know the concept and have never had to prove a fix to somebody who did not believe them.
  • A four-column distribution comparison table — they are writing from documentation. Four programs move independently; no table survives them, and the reader wants to know what their machine does tonight.

A page that has visibly fixed itself, and the half it did not absorb

This site’s own How Packages and Dependencies Work now carries a paragraph that announces itself as a repair: “One correction, because this is the most common error made about package management anywhere” — that the Debian family signs the index while the RPM family signs the packages. It was added after The Life of a Package found it, and it reads as an interpolation into a page that previously said the opposite by omission.

It absorbed the factual half. What it left behind is the diagnostic half: the finding was never really “the two families sign different things”, it was “ask any page what apt install ./foo.deb verifies, and if it does not say nothing, it was not written from the documentation”. The page now tells the reader the correct thing without handing them the question that would let them judge the next page they read.

Which is worth one sentence of self-instruction, and this page has tried to obey it: a correction that does not travel with its test decays back into the corpus it came from.

Before you call it done

  1. Find out what is actually scheduled, and what it is allowed to install. systemctl list-timers --all | grep -Ei 'apt-daily|unattended|dnf-automatic' and apt-config dump | grep -iE 'APT::Periodic|Unattended-Upgrade::'. A timer without a period, or a period without a timer, patches nothing.
  2. On Fedora or RHEL, read /etc/dnf/automatic.conf before enabling anything. If apply_updates still says no, nothing has ever been installed.
  3. Run the hinge command and count the two lists. Every uses deleted line without a matching SVC or PID line is something nothing on this machine will ever restart.
  4. List the mapped files no package owns, and decide who is responsible for each. That list is the part of the machine your package manager cannot patch.
  5. Stop treating the pending-updates count as an alert condition. Alert on growth in it, or on the age of the oldest held package — not on it being non-zero, which it cannot be.
  6. Check the reboot question directlyuname -r against the newest /boot/vmlinuz-* — rather than through a flag file that may never be written.
  7. Prove one CVE, once, by hand, using the changelog method in stage 7. Do it before you need it, so that the day somebody asks, you already know your own machine’s answer.

Related reading

  • The Life of a Package — the eight stages that get the correct file onto the disk in the first place, which is the half this page assumes.
  • Hardening a Public-Facing Service — the other half of the same afternoon, and the page whose one-clause-short sentence this one exists to finish.
  • Securing a New Server — where automatic updates get switched on, and what else belongs in the first hour.