You SSH into a box, run sudo apt update && sudo apt upgrade -y, see “0 upgraded, 0 newly installed”, and close the terminal. The system is patched. The CVE is closed. You move on.
That system is, in many cases, still vulnerable.
apt is a package manager. Its job is to put new files on disk and update package metadata. That is all it does. It does not unload kernel modules from memory. It does not relink shared libraries that are already mapped into running processes. It does not reload microcode from EEPROM into the CPU. It does not rebuild your container images. Every one of those things is your responsibility, and every one of them is a place where “I patched it” silently turns into “I did not, in fact, patch it.”
This article walks through every category of patch that apt upgrade does not finish, how to detect what’s still vulnerable, and how to automate the gap closure.
What apt upgrade Actually Does
When you run apt upgrade, dpkg replaces files on disk and runs the package’s postinst script. The postinst is allowed to restart services it owns — and most well-maintained packages do. nginx, postgres, sshd, openssh-server: their postinst scripts will restart the daemon as part of the upgrade.
That sounds complete, until you realize what postinst cannot do:
What apt upgrade does | What it does NOT do |
|---|---|
Replaces files in /usr, /etc, /lib | Unloads the running kernel from memory |
Updates package metadata in /var/lib/dpkg | Restarts processes that linked an old shared library |
Runs postinst scripts | Reloads CPU microcode |
| Restarts services owned by the upgraded package | Re-execs systemd (PID 1) |
Removes old kernel images (with autoremove) | Rebuilds container images |
Updates the bootloader (grub configs for new kernels) | Re-executes long-running processes that started before the upgrade |
Every row in the right column is a place where the old, vulnerable code is still resident in memory and still serving requests, even though the on-disk version is fixed.
Category 1: The Kernel — The Most Common Hidden Vulnerability
Kernel CVEs are the textbook example. When apt installs linux-image-6.8.0-58-generic, it puts the new kernel image into /boot/vmlinuz-6.8.0-58-generic and updates grub. The kernel that is currently scheduling your processes, however, is whatever was loaded by the bootloader the last time the machine started — possibly a kernel from six months ago with a remotely exploitable nf_tables UAF.
Detection
Ubuntu and Debian set a flag file the moment any package’s postinst decides a reboot is needed:
| |
Compare the running kernel to the installed kernel:
| |
Check the boot history to see how long you’ve been running an old kernel:
| |
An uptime of 200 days on a server that auto-installs kernels every week means you have ~25 kernels worth of CVEs you haven’t actually applied.
/var/run/reboot-required is not authoritative. It is set by Ubuntu’s update-notifier-common package, which only some postinst scripts cooperate with. Microcode packages, glibc, dbus, systemd, and a few others do set it. Many third-party packages do not. Treat the flag as a minimum signal, not a complete one.Live Patching: The Only Real Workaround
If you cannot reboot — and many production fleets cannot, casually — live patching is the only honest answer. There are four serious options:
| Tool | Vendor | Cost | Notes |
|---|---|---|---|
| Canonical Livepatch | Ubuntu (Canonical) | Free for personal Ubuntu Pro (≤5 machines), paid for fleets | Easiest path on Ubuntu, integrates with unattended-upgrades |
| TuxCare KernelCare | TuxCare | Paid | Distro-agnostic (RHEL, Ubuntu, Debian, Oracle Linux, Alma, Rocky), supports library live patching too |
| Oracle Ksplice | Oracle | Free with Oracle Linux Premier Support | Oracle Linux + Ubuntu, the original live patching system |
| kpatch | Red Hat / upstream | Free, OSS | DIY — you build your own patches; almost no one does |
Enabling Canonical Livepatch on Ubuntu:
| |
After this, kernel CVEs deemed live-patchable are applied to the running kernel without a reboot, usually within hours of upstream release.
Live patching is not a permanent reboot replacement. Three things to internalize:
- Not every kernel CVE is live-patchable. Changes to kernel data structures (not just code) cannot be hot-applied. The vendor will tell you “reboot required” anyway for those.
- Live patches accumulate. Each new patch is applied on top of the running kernel’s text segment. After enough of them, troubleshooting becomes painful and the only sane path is a reboot.
- Live patches expire when support ends. Each kernel version has a livepatch support window (Canonical: typically the lifetime of the LTS point release). When it ends, you must reboot to a new kernel.
Treat live patching as extra time to schedule a reboot, not as a way to never reboot.
Category 2: Shared Libraries — The Silent Killer
This is the category that bites the most people, because nothing flags it. Imagine the following sequence:
- nginx starts. The dynamic linker loads
/usr/lib/x86_64-linux-gnu/libssl.so.3from disk into nginx’s address space. - A CVE drops in OpenSSL. You run
apt upgrade. dpkg replaces the file on disk. - nginx is still running. Its memory image still contains the old, vulnerable
libsslcode, mapped from the now-deleted inode. New requests are served by the vulnerable code path. /var/run/reboot-requiredis not set, because dpkg has no way to know nginx loaded the library.
You will pass any “is libssl patched?” check that looks at the on-disk file. You will fail any actual exploitation test, because the running process still holds the old version.
Detection
The Linux kernel keeps deleted-but-still-mapped files visible via /proc/<pid>/maps and lsof. They show up as (deleted) or with the DEL marker:
| |
But the right tool already exists: needrestart. On Ubuntu 22.04+ it’s installed by default and runs automatically after every apt upgrade. If it isn’t, install it:
| |
Using needrestart
| |
Sample output after a libssl upgrade with no manual restart:
| |
The “deferred” services are exactly the ones you need to restart manually — needrestart flagged them, but its default policy is to ask before touching network-facing services. If you want it fully automatic, edit /etc/needrestart/needrestart.conf:
| |
The blacklist is where you should be careful: stateful services need a coordinated restart strategy, not a systemctl restart from a cron job at 3am.
Category 3: Microcode — The Patch You Forget Exists
CPU microcode patches ship as Debian packages: intel-microcode and amd64-microcode. They patch silicon-level bugs (Spectre, Meltdown, Downfall, Reptar, the entire MDS/L1TF/SRBDS family, and more recently INCEPTION, RetBleed, ZenBleed). When apt upgrade installs a new microcode package, it writes the new blobs to /lib/firmware/intel-ucode/ or /lib/firmware/amd-ucode/. The CPU does not load them until next boot.
Worse: some CPUs only accept microcode updates very early in boot (loaded by the bootloader via the initrd’s microcode CPIO header), so a warm reboot may not be enough — you may need a power cycle to load a brand-new revision.
Detection
| |
If /proc/cpuinfo shows a revision lower than the latest in /lib/firmware, your CPU is still running the old microcode. None of the speculation-side mitigations the kernel reports as “active” actually do what they say without the corresponding microcode loaded.
| |
If you see lines like Mitigation: Microcode for one issue and Vulnerable: No microcode for another, that’s a direct signal that a microcode update is sitting on disk, unused.
Category 4: systemd, dbus, and Other PID-1-adjacent Services
systemd is PID 1. You cannot systemctl restart systemd. When apt upgrade ships a new systemd binary, it can’t restart itself — it can only re-execute itself in place:
| |
needrestart will tell you when systemd needs a re-exec. If you ignore it, systemd-managed services will continue running, but the PID-1 process itself is the old, possibly vulnerable binary, and any systemctl command may behave unexpectedly because the systemctl client is talking to a stale daemon.
The same goes for dbus, polkit, and udev. These have direct security implications — polkit in particular has been responsible for several local-root CVEs (pwnkit, etc.), and a missed polkit restart after upgrade leaves the vulnerability exploitable.
Category 5: Containers — Host Patching Does Nothing
This is the biggest blind spot in modern infrastructure. You patched the host. The host’s libssl is fresh. But your containers are still running images built six months ago, and container userspace is fully isolated from the host’s userspace. Every container has its own copy of glibc, openssl, libcurl, libxml2, etc.
| |
If the host shows 3.0.13-0ubuntu3.5 and the container shows 3.0.2-0ubuntu1.18, the container is vulnerable to every OpenSSL CVE published in the last 18 months — regardless of what apt upgrade did on the host.
The Only Real Fix: Rebuild the Image
| |
In a CI/CD pipeline, this should be automated:
- Scheduled job (daily or on base-image webhook) runs
docker pullfor every base image you depend on. - If any base image changed, trigger a CI build of every downstream image.
- Run a vulnerability scanner (
trivy image,grype,docker scout) against the freshly built image. - If the scan passes the policy, push to the registry and trigger a rolling update.
A static nginx:1.21 tag pinned in your Dockerfile for stability is also a static collection of every CVE in nginx 1.21’s dependencies. Pin to a digest, but track the tag’s evolution and rebuild when the upstream digest changes.
I’ve covered the operational side of this in detail in Vulnerability Management: Where to Start and Why Automation Is Non-Negotiable.
Putting It All Together: The Post-Upgrade Verification Script
Here’s a single script you can drop into /usr/local/sbin/post-upgrade-check.sh and run after every upgrade (or via cron):
| |
Run it after every apt upgrade (or wire it into the upgrade itself):
| |
The output is intentionally noisy. Every red line is a real vulnerability that survived your patch run.
Automating the Whole Thing
Combining unattended-upgrades, needrestart, and a controlled reboot strategy gets you most of the way to “actually patched, not just apt-upgraded.” The idea is:
unattended-upgradesinstalls security updates daily.needrestartruns automatically after every apt transaction and restarts safe services.- Reboot windows are scheduled and coordinated, not opportunistic — handled by Ansible (or the orchestrator of your choice) so a load balancer can drain a node first.
- Live patching covers the gap between reboot windows for critical kernel CVEs.
Ansible: Coordinated Reboot After Patching
| |
The serial: 1 is the part most people skip. Without it, every host in the group reboots in parallel. With a load balancer in front, you want exactly one host out of rotation at a time.
If you want this to also apply CIS hardening or set up unattended-upgrades itself, the Ubuntu Hardening article covers that side.
What “Patched” Should Actually Mean
A system is patched when all of the following are true:
| Check | Verification |
|---|---|
| Latest packages installed | apt list --upgradable is empty |
| Running kernel matches installed | uname -r == newest linux-image-* |
| No process holds deleted libraries | lsof +c 0 | grep DEL is empty for libs |
| Microcode loaded matches firmware on disk | /proc/cpuinfo revision == latest in /lib/firmware |
| systemd re-exec’d if updated | systemctl daemon-reexec after a systemd upgrade |
| Containers rebuilt from latest base | image digests refreshed in registry, deployments rolled |
| Speculation mitigations active | /sys/devices/system/cpu/vulnerabilities/* shows no Vulnerable |
| Reboot flag clear | /var/run/reboot-required does not exist |
If any of those are false, the package version on disk is irrelevant. The vulnerable code is still in memory, still serving requests, still exploitable.
apt upgrade is the first step of patching. Treating it as the last step is one of the most common — and most consequential — gaps in real-world Linux operations.
Further Reading
- needrestart(1) — Debian manpage
- Ubuntu Livepatch service documentation
- TuxCare KernelCare technical overview
- Linux kernel speculation mitigations sysfs interface
- Ubuntu Hardening: Automation is one of the key — the same patching strategy as part of a full CIS baseline
- Vulnerability Management: Where to Start and Why Automation Is Non-Negotiable — the program-level view of all this
