Check your server for known CVEs
List every known CVE affecting the packages on your Debian or Ubuntu VPS with debsecan and debvulns, then alert only on the ones that are worth a reboot.
Which known CVEs affect this server right now
To find out which known CVEs affect your server right now, compare the versions of your installed packages against a published vulnerability feed. CVE (common vulnerabilities and exposures) is the public identifier scheme for known security bugs. On Debian that comparison is debsecan or debvulns. On Ubuntu it is pro cves, because Ubuntu keeps its own tracker and the Debian tools read the wrong one.
Applying updates and knowing what you are exposed to are two different jobs. apt upgrade answers one question: is a newer package available. It does not tell you which CVEs are open on this machine, and it does not tell you which of them will never be fixed in your release. The second job needs a scanner, and a scanner needs you to understand exactly what it compares.
What a package scanner actually compares
The mechanism is small enough to hold in your head. The scanner reads the list of installed packages from the dpkg database. It maps each binary package back to the source package it was built from, because vulnerability data is recorded per source package. It downloads a feed that says, for each CVE and each source package, which version fixed the issue in each suite. Then it compares your installed version against that fixed version, using the same version ordering as dpkg --compare-versions.
A CVE scanner on Debian or Ubuntu is a version comparison against a cached feed. Nothing more, and nothing less.
The binary-to-source mapping is the part people skip, and skipping it costs you findings. The openssl source package builds a binary package called libssl3t64 on Debian 13 and Ubuntu 24.04. A CVE filed against openssl will not match a naive search of your binary package names. This is why debvulns asks for python3-apt: the APT cache carries that mapping, and without it the tool falls back to dpkg-query and can miss issues whose source name differs from the binary name.
The feed also carries a per-release status that is not a simple yes or no. Open the tracker page for a source package, for example the openssl entry on the Debian security tracker, and each release column holds one of vulnerable, fixed, no DSA or postponed. no DSA means the security team looked at the issue and decided it does not warrant a stable update, so that fix is not coming to your release. Your scanner will report the CVE on every run for the life of that release, and no amount of apt upgrade will clear it. That is the most common reason people give up on these tools, and it is not a bug in the tool.
Scan a Debian server with debsecan
debsecan is in the Debian archive and needs no external repository.
sudo apt update && sudo apt install -y debsecan
debsecan --suite trixieEach output line is one CVE against one installed binary package:
CVE-2016-2776 bind9-host (remotely exploitable, high urgency)
CVE-2016-8864 libdns-export100 (fixed, remotely exploitable, medium urgency)The urgency and the remotely exploitable marker come straight from the Debian tracker. The word fixed means a corrected version is in the archive, so an upgrade will clear that line today. A line without fixed is one you cannot close.
Pass --suite or half the output is wrong. debsecan uses the suite only to decide whether a corrected package exists and whether a package is obsolete. Get the suite wrong and the list of CVEs is still correct, while the fixed markers and the obsolete-package warnings are not. Use the codename of the release you actually run: trixie for Debian 13, bookworm for Debian 12.
Two views are worth putting in a script:
debsecan --suite trixie --only-fixed --format packages
debsecan --suite trixie --only-fixed --format detail--format packages prints only binary package names, one per line, which is the list you can act on. --format detail prints everything the feed holds for each issue. --only-fixed drops the no DSA entries described above, so the output shrinks to work you can finish.
For the scheduled run, edit /etc/default/debsecan and set SUITE, MAILTO and REPORT. The package ships debsecan-create-cron, which writes a cron entry that fires hourly at a randomly chosen minute. The hourly entry is not an hourly scan: debsecan --cron checks whether the data was already downloaded today and exits if it was. The random minute exists so that every Debian machine does not hit the tracker in the same second.
Why debsecan reports the wrong thing on Ubuntu
debsecan is in Ubuntu universe (version 0.4.20.1 in 24.04). It installs cleanly and it prints confident output you should not trust. Its feed is the Debian security tracker, and that tracker does not track Ubuntu. Ubuntu-only issues are absent from it. Ubuntu version strings such as 3.0.13-0ubuntu3.4 never match a Debian fixed version, and debsecan compares versions it does not recognise against Debian unstable. The result is alarms for issues Canonical patched months ago, and silence on issues that are real. Launchpad bug #95925 has recorded this since 2007.
A bridge used to exist. The ust2dsa project converted Ubuntu CVE tracker data into debsecan's format and republished it every six hours. It was archived in October 2021. Pointing --source at it now gives you a feed that stopped moving five years ago, which is worse than having no scanner, because a stale feed reports clean.
List the CVEs on an Ubuntu server with pro cves
Ubuntu ships the answer in ubuntu-pro-client. Version 35 added pro cves, which lists the installed packages affected by a known CVE.
pro cves
pro cves --fixable
pro cves --unfixablePackage Priority Origin Vulnerability
firefox medium esm-infra CVE-2020-6852
openssh low standard CVE-2021-3188
vim critical esm-infra CVE-2011-3374
vim-tiny high - CVE-2011-3380Read the Origin column first, because it decides what you can do. standard means the fix is in the ordinary Ubuntu security pocket and a normal upgrade installs it. esm-infra or esm-apps means the fix exists only in Expanded Security Maintenance, which needs an Ubuntu Pro subscription attached to the machine. A - means no fix exists anywhere yet, and that line is the Ubuntu equivalent of Debian's no DSA.
For a single CVE, two commands give you the full picture:
pro cve CVE-2024-5480
pro fix --dry-run CVE-2020-25686pro fix --dry-run changes nothing and tells you exactly where you stand:
CVE-2020-25686: Dnsmasq vulnerabilities
- https://ubuntu.com/security/CVE-2020-25686
1 affected package is installed: dnsmasq
(1/1) dnsmasq:
A fix is available in Ubuntu standard updates.
{ apt update && apt install --only-upgrade -y dnsmasq }
✔ CVE-2020-25686 is resolved.A machine that is not affected prints No affected source packages are installed. instead. A machine with no fix available prints a line such as ✘ CVE-2017-9233 is not resolved. Drop --dry-run and the same command performs the upgrade. pro security-status is the companion view: it counts your installed packages by repository and reports how many security updates are pending, which matters because Main and Universe carry different support promises.
Install debvulns, and pin the version
debvulns is a Python toolkit built on the Debian security tracker. It gives you a command line scanner and a Prometheus exporter over the same data. Version 0.2.2 is current as of August 2026.
Running pip install debvulns on Debian 12 or newer fails with error: externally-managed-environment, because the system Python belongs to dpkg. Install into a virtual environment that can still see the system python3-apt:
sudo apt update && sudo apt install -y python3-venv python3-apt
sudo python3 -m venv --system-site-packages /opt/debvulns
sudo /opt/debvulns/bin/pip install debvulns==0.2.2
/opt/debvulns/bin/debvulns --severity high --format json | head -40--system-site-packages is the flag that matters. Without it the virtual environment cannot import python3-apt, debvulns falls back to dpkg-query, and you lose the binary-to-source mapping with no error message. Pin the version too. A scanner whose behaviour changes under you between two Tuesdays is a scanner you cannot compare against last week's run.
The flags you will use:
--severityacceptscritical,high,medium,lowornegligibleand filters the output.--formatacceptsjsonorcsv. JSON is the default.--sort-byacceptspackageorcve.--suiteoverrides the auto-detected Debian codename.--no-cacheskips the cached feed and downloads a fresh one.-vlogs what the tool is doing to stderr, including the URLs it fetched.
debvulns also downloads EPSS (exploit prediction scoring system) scores and attaches one to each finding. EPSS is a published daily estimate of the probability that a CVE will be exploited within the next thirty days. It is a useful second opinion next to a CVSS (common vulnerability scoring system) base score, because CVSS describes how bad the bug would be if used, while EPSS describes how likely anyone is to bother.
The same warning applies as for debsecan. The feed is Debian's, so run debvulns on Debian. On Ubuntu, pro cves is the tool that matches your archive.
Export the CVE count to Prometheus
If you already run monitoring, the exporter turns this into a number you can alert on instead of a report nobody opens.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin debvulnsWrite /etc/systemd/system/debvulns-exporter.service:
[Unit]
Description=debvulns Prometheus exporter
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=debvulns
ExecStart=/opt/debvulns/bin/debvulns-exporter --port 9222 --refresh-interval 21600 --cache-dir /var/cache/debvulns-exporter
CacheDirectory=debvulns-exporter
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
Restart=on-failure
RestartSec=30
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now debvulns-exporter
curl -s localhost:9222/-/ready
curl -s localhost:9222/metrics | grep -c debvulns_CacheDirectory=debvulns-exporter is what makes ProtectSystem=strict workable: systemd creates /var/cache/debvulns-exporter owned by the service user, and that is the only writable path the process gets. /metrics returns HTTP 503 until the first scan finishes, and /-/ready returns 200 only once it has. That is the exporter refusing to publish a zero it has not measured. /-/healthy returns 200 as soon as the process is alive, so use that one for liveness and /-/ready for scrape readiness.
The metrics worth building rules on:
debvulns_vulnerabilities_totalis the aggregate count, labelled by severity, by whether a fix is available, and by whether the issue is remotely reachable.debvulns_vulnerability_infois one series per CVE and package, carrying the CVE id as a label.debvulns_vulnerability_epss_scoreis the EPSS probability for each CVE.debvulns_scan_statusis 1 when the last scan succeeded and 0 when it failed.debvulns_last_scan_timestamp_secondsrecords when that scan ran.
Read the exact label values off your own box before you write a rule, because a selector that matches nothing never fires and looks identical to a healthy server:
curl -s localhost:9222/metrics | grep '^debvulns_vulnerabilities_total'Then write two rules:
groups:
- name: debvulns
rules:
- alert: FixableCriticalCVE
expr: sum by (instance) (debvulns_vulnerabilities_total{severity="critical", fix_available="true"}) > 0
for: 6h
annotations:
summary: "A critical CVE with an available fix has stayed open for six hours"
- alert: CVEScanStale
expr: time() - debvulns_last_scan_timestamp_seconds > 172800
for: 30m
annotations:
summary: "CVE data on this host is more than two days old"The for: 6h clause is what keeps the first alert credible. Without it you are paged for a CVE that your automatic upgrade run would have closed twenty minutes later, and an alert that resolves itself before you finish reading it trains you to ignore the next one. With it, you hear only about issues that survived automatic patching.
The second rule is the one people forget. A scanner that stopped scanning reports zero vulnerabilities, and zero looks exactly like healthy on a dashboard. Alert on the age of the data as well as its contents.
What a package scanner cannot see
This boundary is sharper than most people expect. These tools compare installed package versions against a vulnerability feed, and everything they know comes from the dpkg database. Anything that did not arrive through the package manager is invisible.
That excludes:
- Everything inside a Docker image. The scanner sees the
docker.iopackage on the host and knows nothing about the userland, the interpreter or the libraries inside your containers. - Every
pip,npm,cargoandgodependency, including the ones an image pulled at build time. - Static binaries, and anything installed from a tarball or a
curl ... | shline, because they carry no package metadata at all. - Software you compiled yourself, even when it is the same version as the distribution package.
For images, scan them where they are built, with an image scanner, not on the host. For language dependencies the risk model is different anyway: the common failure is a hijacked or malicious package rather than an old version with a known CVE. npm supply chain attacks on a server covers that half of the problem, and it is not the half debsecan was ever going to catch.
Your answer is only as fresh as the cached feed
Every tool here caches. debvulns writes to /var/cache/debvulns when it can and falls back to ~/.cache/debvulns when it cannot, and it re-downloads only when the cache is more than 24 hours old. debsecan --cron downloads once per calendar day. The exporter defaults to a 24 hour refresh interval and refuses any value below 3600 seconds.
So an answer can be a day old, and a broken network gives you an old answer instead of an error. Force a fresh read when you are investigating something specific:
/opt/debvulns/bin/debvulns --no-cache --severity critical
ls -l --time-style=long-iso /var/cache/debvulnsThe CVEScanStale rule above does this for you across every host. It is the difference between "no critical CVEs" and "no data since Thursday", which are the same picture on a graph and very different situations.
When does a CVE justify a reboot
A fixed package on disk is not a fixed process in memory. apt upgrade replaces the file, but a long-running process keeps using the copy it mapped at start. needrestart finds those processes.
sudo apt install -y needrestart
sudo needrestart -r l
sudo needrestart -b -k-r l lists what needs restarting without touching anything. -b is batch mode, which prints machine-readable lines instead of a dialog:
NEEDRESTART-KCUR: 6.12.48-amd64
NEEDRESTART-KEXP: 6.12.57-amd64
NEEDRESTART-KSTA: 3KCUR is the kernel you are running. KEXP is the newest kernel installed on disk. When they differ, KSTA reports 3, which means a reboot is pending. needrestart -o emits the same facts in OpenMetrics format if you would rather scrape them than parse them.
Ubuntu also creates /var/run/reboot-required when a package's post-installation script says a reboot is needed, and names the responsible packages in /var/run/reboot-required.pkgs. That path lives on a tmpfs, so it disappears on every boot by design. pro system reboot-required answers the same question in one word, and it reports separately when a livepatch has already been applied to the running kernel. Debian creates no such file by default. Install reboot-notifier (version 0.12 in Debian 13) for the compatible equivalent, or use needrestart, which behaves the same on both distributions.
The decision rule, in the order you should apply it:
- The CVE is in a shared library, the fixed package is installed, and
needrestart -r lnames services still using the old copy. Restart those services. No reboot needed. - The CVE is in the kernel and
KCURdiffers fromKEXP. Only a reboot swaps the running kernel, unless you use live kernel patching on a VPS, which covers the subset of kernel fixes that can be applied to a running system. - The CVE has no fix in your release, shown as
no DSAon Debian or anOriginof-on Ubuntu. Rebooting changes nothing. Record the exposure, or mitigate it by turning the affected feature off. - The scanner lists the CVE but nothing reachable uses the package. Fix it on the ordinary patch cycle. A flaw in a library that only your local
mancommand links against is not an incident.
The cadence that survives a quiet month
Pick a rhythm you will still be running in six months, because the value of this work is entirely in its repetition.
Run the scan daily and unattended. On Debian that is the debsecan cron entry with REPORT set, or the exporter's own refresh loop. Let unattended upgrades install the security fixes on their own schedule, so the scanner's job is to report what automation could not close rather than to be your only defence.
Alert on one number, not on the report. Fixable issues at critical severity, held for six hours, is a threshold most single servers cross a few times a year. That is rare enough that you will actually read the alert when it arrives. Everything else belongs on a dashboard you review during your monthly server maintenance pass.
Write the reboot rule down before you need it, because you will be applying it at an awkward hour. A kernel CVE with a differing KEXP means schedule a reboot. A library CVE means restart the services needrestart names. No fix available means log it and check again next month.
Re-read your exclusions once a quarter. The list of things living outside dpkg only grows: a container someone added, a binary dropped into /usr/local/bin. Each one is a place where your scanner reports clean because it cannot see. On a fleet, keep that inventory alongside whatever you use for managing multiple Linux servers, so the exceptions are visible to more than one person.
FAQ
How do I list the CVEs affecting my Debian server?
Install debsecan and run debsecan --suite trixie, using the codename of the release you actually run. Each output line names one CVE and one installed binary package, and the word fixed in the parentheses means a corrected version is already in the archive. Add --only-fixed --format packages to reduce the output to the package names you can upgrade today. For JSON output and EPSS scores, install debvulns into a virtual environment created with --system-site-packages so it can import python3-apt.
Why does my scanner keep reporting a CVE that apt cannot fix?
Because the Debian security team marked that issue no DSA for your release, meaning they judged it too minor for a stable update, so no fixed package is coming. The Ubuntu equivalent is an Origin of - in pro cves output. The CVE is genuinely open on your machine and the scanner is right to keep listing it. Use debsecan --only-fixed or pro cves --fixable to see only what you can act on, and review the remainder on your normal maintenance pass.
Does a CVE scanner see vulnerabilities inside my Docker containers?
No. These tools read the host's dpkg database only, so they see the docker.io package and know nothing about the operating system or libraries inside your images. The same blind spot covers every pip or npm dependency, plus anything installed from a tarball. Scan container images where they are built, using an image scanner, and treat language dependencies as a separate problem with a different failure mode.
When does a CVE actually require a reboot?
Only when the fix is in the kernel. Run sudo needrestart -b -k and compare NEEDRESTART-KCUR, the running kernel, against NEEDRESTART-KEXP, the newest one installed. If they differ, NEEDRESTART-KSTA reports 3 and a reboot is pending. For a CVE in a shared library the fixed file is already on disk, and you only need to restart the processes still mapping the old copy, which sudo needrestart -r l will name for you.
Can I run debsecan on an Ubuntu server?
It installs from universe and its answers are wrong. debsecan reads the Debian security tracker, which does not track Ubuntu issues and does not recognise Ubuntu version strings such as 3.0.13-0ubuntu3.4, so you get false alarms for issues Canonical patched long ago and silence on Ubuntu-only issues. The ust2dsa bridge that once translated Ubuntu data into debsecan's format was archived in October 2021 and its feed no longer updates. Use pro cves on Ubuntu.