A system updates audit checks whether your Linux server has pending patches, whether security updates apply automatically, whether the running kernel matches the latest installed version, and whether a reboot is needed to load updated libraries into memory. This page documents the 4 checks in the CtrlOps System Updates audit, in the order the audit runs them.
A security patch was released three weeks ago and this server still runs the old kernel. Automatic updates were never configured. The reboot flag has been sitting there since an apt upgrade nobody scheduled a window for. That gap between "patch available" and "patch actually running" is what these four checks measure. Every threshold below is transcribed from the audit script itself, not from general advice.
Key takeaways
Four checks cover the whole patch lifecycle: are updates available, will they apply without you, is the kernel current, and does the host need a restart to finish applying what is already installed. Passing all four means no pending patches, automatic updates on, the newest kernel running, and nothing stale left in memory.
- One check is rated HIGH, pending updates. A published CVE with an available patch that has not been applied is not a theoretical risk, it is a documented exploit path that is open right now.
- No check needs root. All four read package manager metadata and system state files that any user can see, which makes this the only audit in the set that returns a complete answer from an unprivileged session.
- The whole audit takes about 8 seconds when CtrlOps runs it over your existing SSH connection, against roughly 10 to 15 minutes by hand.
- A pending reboot means the patches are not applied yet. The kernel and shared libraries loaded at boot stay in memory until you restart. New code on disk that nothing is executing protects nothing.
| # | Check | Severity | What it catches | Root needed |
|---|---|---|---|---|
| 1 | Pending updates | HIGH | Uninstalled security and general package updates | No |
| 2 | Automatic security updates | MEDIUM | Missing or unconfigured unattended-upgrades / dnf-automatic | No |
| 3 | Kernel up to date | MEDIUM | A newer kernel installed but not booted into | No |
| 4 | Reboot required | MEDIUM | A pending reboot for updated kernel or core libraries | No |
apt update would be a write operation and every check in this catalog is strictly read-only. On a server that has not refreshed in three weeks, check 1 can report a clean PASS while a dozen security patches sit waiting upstream. Run sudo apt update (or sudo dnf makecache) before you trust a pass here. The script says so in its own result text for exactly this reason.Check 1: Are there pending updates?
Pending security updates are the most common cause of preventable server compromises. A published CVE with an available patch is not a hypothetical: it is a known vulnerability with a documented exploit path and, usually, public proof-of-concept code.
The audit detects the package manager and adapts. On apt it runs a simulated upgrade (apt-get -s upgrade), counts the Inst lines, and counts how many of them mention a security origin. On dnf and yum it runs check-update and counts package lines. On zypper it reads list-updates and list-patches. On apk it lists outdated packages.
How to check manually
# Refresh first, or the count below is meaningless
sudo apt update
# Debian/Ubuntu: what is pending
apt-get -s upgrade 2>/dev/null | grep '^Inst '
# ...and how many of those are security
apt-get -s upgrade 2>/dev/null | grep '^Inst ' | grep -ci security
# RHEL/Rocky/Alma
dnf check-update
dnf updateinfo list security
# SUSE
zypper list-updates
zypper list-patches| Result | When | What it means |
|---|---|---|
| PASS | No pending updates according to the package metadata already on the host | Everything available is installed. The result text reminds you to refresh the lists for a current view, because this check does not refresh them itself. |
| FAIL | One or more pending updates are identified as security updates | The security count and the total are both reported. Apply now. Each one is an open, documented exploit path. |
| WARN | Updates are pending but none is identified as a security update | The count is reported. Schedule an upgrade anyway: general updates routinely carry fixes that were never tagged as security. |
| SKIP | No supported package manager was found, or the check-update command failed | Nothing was measured, so nothing is claimed. |
The FAIL branch does not exist on every distribution, and this is worth knowing before you trust a WARN. Separating security updates from ordinary ones requires the package manager to expose that distinction cheaply, and only three of the five do it here: apt, yum and zypper. On dnf and apk, every pending update reports as a WARN regardless of what it fixes. So a WARN on a Fedora or RHEL 8+ host is not evidence that nothing security-related is waiting; run dnf updateinfo list security yourself to find out.
On apt, the security count comes from a case-insensitive match on the simulated upgrade lines, which carry the origin suite. It is a good heuristic rather than an authority, and a package backported into a non-security pocket will not be counted.
How to fix it
# Debian/Ubuntu: preview, then apply
apt-get -s upgrade # simulate, changes nothing
sudo unattended-upgrades --dry-run # test the security-only path
sudo unattended-upgrades # apply security updates only
sudo apt update && sudo apt upgrade # everything
# RHEL/Rocky/Alma
sudo dnf upgrade --security
sudo dnf upgrade
# SUSE
sudo zypper patch
sudo zypper updateapt-get -s upgrade simulates and shows exactly what changes. Major version movements in glibc, OpenSSL or a language runtime can break running applications in ways that surface hours later. The workable split is to apply security updates immediately and without ceremony, and to schedule everything else into a maintenance window where you can watch it.Check 2: Are automatic security updates enabled?
Manual patching works on the server you log into every day. It fails on the one you configured six months ago and check quarterly. Automatic security updates shrink the window between "patch released" and "patch applied" from weeks to hours, without anyone remembering to do anything.
The audit checks the mechanism appropriate to the package family: unattended-upgrades on Debian and Ubuntu, the dnf-automatic timer on RHEL family, and the YaST or cron arrangements on SUSE.
How to check manually
# Debian/Ubuntu: is the package installed?
dpkg -l unattended-upgrades 2>/dev/null | grep '^ii'
# ...and is the periodic run actually switched on? The value matters.
grep -rs 'APT::Periodic::Unattended-Upgrade' /etc/apt/apt.conf.d/
# RHEL/Rocky/Alma
systemctl is-enabled dnf-automatic.timer| Result | When | What it means |
|---|---|---|
| PASS | unattended-upgrades is installed with APT::Periodic::Unattended-Upgrade "1" set, or the dnf-automatic timer is enabled | Security patches will apply without anyone logging in. |
| WARN | The package is installed but the periodic run is not enabled, or dnf-automatic is not enabled, or this is a SUSE host | The mechanism exists but is not doing anything yet. Configure it. |
| FAIL | unattended-upgrades is not installed on a Debian or Ubuntu host | A zero-day patch waits for your next manual login. Install it. |
| SKIP | Alpine, which ships no unattended-upgrade daemon, or no supported mechanism was detected | Schedule apk upgrade via cron if you want automatic patching there. |
Three things about how this result varies by distribution, none of them obvious from the matrix alone. FAIL only ever happens on Debian and Ubuntu, because that is the only branch that distinguishes "not installed" from "not enabled"; on RHEL family, a host with no dnf-automatic at all reports the same WARN as one where the timer is simply off. A SUSE host can never PASS this check: both zypper branches warn, one telling you to verify the YaST arrangement and one telling you to create one. And the apt branch requires the value "1" specifically, so a host with APT::Periodic::Unattended-Upgrade "0" in place is a WARN, not a PASS. That last one is the most common half-configured state there is: the package is present, the config file exists, and nothing runs.
How to fix it
# Debian/Ubuntu
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# Confirm both lines are "1"
cat /etc/apt/apt.conf.d/20auto-upgrades
# APT::Periodic::Update-Package-Lists "1";
# APT::Periodic::Unattended-Upgrade "1";
# RHEL/Rocky/Alma
sudo dnf install dnf-automatic
sudo systemctl enable --now dnf-automatic.timer
# And make it actually install, not just download and email
sudo sed -i 's/^apply_updates = no/apply_updates = yes/' /etc/dnf/automatic.confunattended-upgrades --dry-run, and note the dnf-automatic gotcha above: the shipped default downloads and notifies without installing, so an enabled timer with apply_updates = no passes this check while patching nothing.Check 3: Is the running kernel up to date?
When a kernel update installs, the new image is written to /boot and the old kernel stays in memory until the machine restarts. Package tooling reports the new version as installed and uname -r still shows the old one. Every kernel CVE fixed in the new version remains exploitable in the meantime.
The audit compares uname -r against the highest-versioned kernel package actually installed, reading dpkg on Debian family, rpm (falling back to kernel-core) on RHEL family, and apk for linux-lts or linux-virt on Alpine.
How to check manually
# What is running
uname -r
# What is the newest installed
# Debian/Ubuntu
dpkg -l | awk '/^ii +linux-image-[0-9]/ {print $2}' | sed 's/^linux-image-//' | sort -V | tail -1
# RHEL/Rocky/Alma
rpm -q kernel --qf '%{VERSION}-%{RELEASE}.%{ARCH}\n' | sort -V | tail -1| Result | When | What it means |
|---|---|---|
| PASS | The running kernel string matches the newest installed kernel exactly | The running version is named in the result. This host is on the newest kernel it has on disk. |
| WARN | The running kernel differs from the newest installed one | Both versions are reported. A newer kernel is on disk and inert. Reboot to load it. |
| SKIP | The newest installed kernel could not be determined | The running kernel is still reported. Package query failed, or the kernel package naming is non-standard. |
Note carefully what this compares: installed against running, not available against installed. A server that has never downloaded a newer kernel passes this check happily while three kernel updates wait in the repository. Check 1 is what catches that, and the two only tell the full story read together: check 1 says whether a patch exists, check 3 says whether the one you already fetched is actually executing.
How to fix it
# Reboot in a maintenance window
sudo shutdown -r 02:00
# Or now, if the downtime is acceptable
sudo reboot
# Confirm afterwards
uname -rCheck 4: Is a reboot required?
Beyond the kernel, shared libraries such as glibc, OpenSSL and libsystemd are mapped into memory when a process starts. Updating them writes new files to disk while every already-running process keeps executing the old code. Your web server can be linked against a patched OpenSSL on disk and a vulnerable one in memory at the same time.
The audit reads /var/run/reboot-required on Debian family, runs needs-restarting -r where it exists on RHEL family, and otherwise falls back to comparing uname -r against the newest vmlinuz- image in /boot.
How to check manually
# Debian/Ubuntu: does the flag exist?
ls /var/run/reboot-required 2>/dev/null
# Which packages asked for it?
cat /var/run/reboot-required.pkgs 2>/dev/null
# RHEL/Rocky/Alma
needs-restarting -r; echo "exit=$?" # 0 = no reboot needed
# Any distribution, as a fallback
uname -r
ls /boot/vmlinuz-* | sort -V | tail -1| Result | When | What it means |
|---|---|---|
| PASS | No reboot flag exists, or needs-restarting reports none needed | Everything updated is loaded. This host runs the code it has on disk. |
| WARN | The reboot flag exists, needs-restarting says one is needed, or the running kernel differs from the newest image in /boot | The packages that triggered it are listed where available. Schedule a maintenance window. |
| SKIP | No reboot indicator exists on this distribution and the /boot comparison was inconclusive | The audit fell back to comparing against /boot and could not settle it. Verify with your package manager. |
On a WARN, the result names the packages from /var/run/reboot-required.pkgs, truncated to the first hundred characters. That list is the useful part, because it tells you which of two very different situations you are in: a kernel entry means only a full reboot will do, while a list of libraries means you may be able to restart the affected services instead.
How to fix it
# Schedule the restart
sudo shutdown -r 02:00
# Or find out whether services alone will do
sudo needs-restarting -s # RHEL/Rocky/Alma
sudo checkrestart # Debian/Ubuntu, from debian-goodies
sudo systemctl restart nginx postgresqlneeds-restarting -s or checkrestart will tell you which ones. If the update was the kernel, no amount of service restarting helps: the kernel is the thing running everything else, and only a boot replaces it. Check 3 is how you tell the two cases apart before you plan the window.What this audit does not cover
Being explicit about scope is part of the point of publishing thresholds. This audit reads the patch state of your OS packages, and does not replace a vulnerability management program.
- Package metadata freshness. The audit reads existing metadata and never refreshes it, because refreshing writes to disk. This is the single most important limitation on the page: a stale-metadata PASS looks identical to a genuine one.
- Application dependencies. npm, pip, gem, composer and cargo packages are invisible here. Use
npm audit,pip-auditor Dependabot for those. - Container images. Base images carry their own package state that host tooling never sees. Trivy or Grype scan those.
- Configuration drift. A fully patched package with an insecure configuration is still exposed, and that is what the other checklists in this section cover.
- Zero-days. This audit finds missing patches. A vulnerability with no patch yet cannot be found by checking for updates, and network segmentation, WAF rules and monitoring are the controls for that window.
- End-of-life distributions. This is the failure mode most likely to bite you, because it inverts the result: a host running Ubuntu 18.04 or CentOS 7 receives no updates at all, so it reports a clean PASS on check 1 while being the most vulnerable server you own. Check your release against its support date; no automated update count will tell you.
The manual audit problem at scale
Running all 4 checks by hand takes roughly 10 to 15 minutes per server: refreshing lists, counting what is pending, separating security from routine, comparing kernel strings, and reading reboot state through whichever tool your distribution provides. It is the shortest audit in the VPS set, and the one where the per-server time matters least and the fleet-wide picture matters most.
| Task | Manual, per server | CtrlOps |
|---|---|---|
| Count pending updates | About 3 min | Included |
| Verify auto-update config | About 3 min | Included |
| Compare kernel versions | About 2 min | Included |
| Check reboot status | About 2 min | Included |
| Total | About 10 to 15 min | About 8 seconds |
| Scored report and PDF export | Not available | One click |
Patch state is the one audit category where the fleet view is the whole point. One server with three pending security updates is a task. Six servers where only two have unattended-upgrades enabled is a pattern, and the pattern tells you something the individual results do not: those four hosts will drift again next month, and the month after, until somebody fixes the mechanism rather than the symptom.
The same applies to reboots. Every host accumulates a pending-reboot flag eventually; what you actually need to know is which ones have been carrying it for six weeks. A single audit run cannot tell you that. That is the server management drift problem again, and patching is where it compounds fastest, because the gap grows on its own without anyone touching the server.
How CtrlOps runs all 4 checks in one click
Instead of running these commands on every server by hand, CtrlOps runs the whole System Updates audit over the SSH connection you already have open. Nothing is installed on the server: no agent, no daemon, no package, no new credentials. The checks are read-only POSIX shell that executes, streams its results back, and leaves nothing behind.
Watch it run
The recording below walks through the SSH & Access audit rather than this one, but the flow is identical for every audit in the catalog: pick it, watch the checks stream in, read the score, send the failures to the AI Terminal. The written steps that follow cover the same ground if you would rather read than watch.
Step 1: Open the Audit tab
Connect to your server in CtrlOps and open the Audit tab in the left sidebar.
Step 2: Select System Updates
Choose the System Updates audit from the catalog. All 4 checks are listed with their descriptions, severity levels and the estimated run time (about 8 seconds in total). Toggle off any check you want to skip.
Step 3: Run it
The audit runs over your existing SSH session. Results stream in as they complete, so you watch each check pass, warn or fail rather than waiting on a final report.
Step 4: Read the report
You get a summary containing:
- A hardening score out of 100
- A severity breakdown: how many HIGH, MEDIUM and LOW findings
- Pass, warning, failed and skipped counts
- A findings table you can sort and filter
Step 5: Fix what failed
Select the failed findings and send them to the AI Terminal. Each proposed fix command appears with an explanation of what it changes. Nothing runs until you review and approve it. This is also where the apt update this audit deliberately does not run belongs: approve it, then re-run the audit against fresh metadata.
Step 6: Re-run and compare
Run the same audit again after applying the fixes and watch the hardening score move. On this audit the stored history answers the question a single run cannot: how long has this host been carrying a pending reboot.
Credentials stay on your machine, stored locally, never synced to the cloud. Every command the AI Terminal proposes is shown before execution and gated behind your approval, never auto-run.
Conclusion
Patching is the least glamorous and most reliably effective control there is. The four checks on this page track a single question through its whole lifecycle: is a patch available, will it install itself, did the kernel it replaced actually get unloaded, and is anything still running the old code from memory. A host can fail any one of those while looking fine on the other three.
Two caveats are worth carrying away, because both make a PASS mean less than it appears. This audit does not refresh package metadata, so run apt update before you trust check 1. And an end-of-life distribution reports zero pending updates forever, which reads as perfect and is the opposite.
The neighbouring checklists cover the layers around this one: SSH and access control, the firewall and network perimeter, file system permissions, services and processes, logging and monitoring and the application and database layer.
Frequently asked questions
It checks four things: whether OS package updates are pending, whether security updates apply automatically, whether the running kernel matches the newest installed one, and whether a reboot is needed to load updated code into memory. The point of the last two is that a patch which is downloaded and installed is not yet a patch that is running, and only a restart closes that gap.
Run sudo apt update first, then apt-get -s upgrade | grep '^Inst ' | grep -i security to list the pending security updates. The -s flag simulates and changes nothing. Apply security updates alone with sudo unattended-upgrades, or everything with sudo apt upgrade. The refresh step matters more than it looks: without it you are counting against whatever metadata the host last downloaded.
Because every check in this catalog is strictly read-only, and refreshing package lists writes to disk. That constraint is what makes the audit safe to run on production without a change window, and it is also this check's main limitation: a host that has not refreshed in weeks can report a clean pass. Run the refresh yourself, or approve it through the AI Terminal, and re-run.
No, and this is the one distribution-specific behaviour worth knowing. Separating security updates from ordinary ones is only done on apt, yum and zypper. On dnf (Fedora, RHEL 8+) and on Alpine, every pending update reports as a warning regardless of what it fixes. So a warning on a dnf host is not proof that nothing security-related is waiting: run dnf updateinfo list security to check.
Yes, for security updates specifically rather than full distribution upgrades. Configure unattended-upgrades on Debian and Ubuntu or dnf-automatic on RHEL family, and verify the setting rather than the package: the most common half-configured state is the package installed with the periodic run switched off, or dnf-automatic enabled with apply_updates = no, which downloads and emails without installing anything.
The new kernel is written to /boot, but the old one stays in memory running everything until the machine restarts, so uname -r keeps reporting the old version and every CVE fixed in the new one stays exploitable. Shared libraries behave the same way at a smaller scale: glibc and OpenSSL are mapped in when a process starts, so a running service keeps using the old copy until it is restarted.
For shared library updates, yes. needs-restarting -s on RHEL family or checkrestart from debian-goodies on Debian and Ubuntu will tell you which services are still linked against replaced libraries, and restarting those resolves it. For a kernel update, no: the kernel is what runs everything else, and only a boot replaces it. Read /var/run/reboot-required.pkgs to see which case you are in.
No. All four checks read package manager metadata, /proc, /boot and config files under /etc that are readable by any user, which makes this the only audit in the set that gives a complete answer from an unprivileged session. Applying the fixes needs root, but finding out what needs fixing does not.
apt upgrade installs everything pending, security and otherwise, and only when you run it. unattended-upgrades is a scheduled mechanism that applies only security-tagged updates by default and needs nobody present. They are complementary: unattended-upgrades for daily automatic patching, apt upgrade inside a maintenance window for the rest.
Not necessarily, for two separate reasons. If the package lists have not been refreshed, you are reading stale metadata and the real answer is unknown. And if the distribution has reached end of life, Ubuntu 18.04 or CentOS 7 for instance, no updates will ever appear because none are being published, so a permanent clean pass is exactly what a critically outdated server looks like. Check your release against its support end date separately.
Daily is reasonable, and cheap: the whole audit takes about 8 seconds per host and needs no privileges. Patch state is the fastest-drifting thing this section measures, because the gap widens on its own without anyone touching the server. Running it after every deployment costs nothing and catches the reboot flag your last upgrade set.
Yes. All four checks work on any Linux host reachable over SSH: EC2, DigitalOcean Droplets, Linode, Vultr, Hetzner, bare metal or any other VPS. The audit detects the package manager (apt, dnf, yum, zypper or apk) and adapts every command accordingly, including the kernel query, which reads dpkg, rpm with a kernel-core fallback, or apk depending on what it finds.