← All articles

Most of your container CVEs aren't yours: base images and what to do about them

August 7, 2026 · Wilson Santos

Here is a measurement from a repository built to be scanned. The application is 35 lines of Python, and scanning it produces 292 findings, split like this:

ControlFindings
images219
iac40
sca17
sast15
secrets1

Three quarters of everything comes from the container image. The application code, the part somebody wrote, reviewed and is responsible for, accounts for fifteen.

Where the findings come from A single bar divided by control. The images control accounts for 219 of 292 findings, the Debian userland inside python:3.8-slim, while iac, sca, sast and secrets together account for 73. 292 findings from one 35-line Python application images · 219 iac · 40 everything else · 73 iac 40 · sca 17 · sast 15 · secrets 1 the base image you chose in one line not fixable by changing the application
Three quarters of the list is one decision, not three quarters of the work.

And the 219 are real, published CVEs in real packages, not a scanner malfunctioning. The image is python:3.8-slim, and what the scanner found is the Debian userland inside it: the C library, the TLS stack, the compression libraries, the certificate bundle. Every one of them is vulnerable to something.

None of them is fixable by changing the application.

Why this shape is normal#

A container image is an operating system with your code on top. The ratio between those two things is not close: a slim Python base is several hundred packages, and your service is one. When a scanner reports "the image has 219 vulnerabilities", it is mostly reporting on a Linux distribution that somebody else assembled and that you selected in one line of a Dockerfile.

This is why container scanning has a reputation for noise it does not entirely deserve. The findings are accurate, and it is the implied call to action that is wrong. A list of 219 CVEs sorted by severity reads as 219 pieces of work, and it is not, because it is roughly one decision, which is which base image, plus a small number of genuine tasks.

The move that changes the number#

Not triage, but a different base.

Rebuilding on a current, minimal base image is the single action that moves this figure, and it usually moves it by an order of magnitude rather than a few percent. The mechanism is boring: fewer packages means fewer things to be vulnerable, and a maintained tag means the ones present are patched.

The options, roughly in order of how much they remove:

  • Update the tag you already use. python:3.8-slim is old. Much of that 219 is the passage of time, and a newer patch tag of the same family removes a large share for the cost of one line and a test run.
  • Go smaller. -slim is already smaller than the default. Distroless and minimal-by-design images go further by removing the shell, the package manager and everything else nobody in production needs.
  • Build from a maintained minimal base. Distributions built specifically to carry near-zero known CVEs exist, and are worth evaluating on their own terms rather than because a vendor said so.

The point is not the specific choice but that the lever is at build time, in the FROM line, and no amount of finding-by-finding triage substitutes for pulling it.

What is left after you pull it#

A smaller number and a more interesting one, because this is what remains after the base image stops dominating.

  • Vulnerabilities in packages you installed. These are yours. They behave like dependency findings because that is what they are.
  • Vulnerabilities in the base that have no fix. No patched version exists yet. Nothing to do but decide, record the decision, and revisit, which is risk acceptance, not triage.
  • Vulnerabilities in code that never runs. The certificate bundle you do not parse, the compression format you never decompress, so it is present, real, and irrelevant to your image.

That third category is the one the industry is still working out how to express, and it has two answers worth knowing.

VEX, and the honest limits of it#

VEX, or Vulnerability Exploitability eXchange, is a document that says this product is not affected by this CVE, and here is the justification, which is how an image publisher tells you that half the CVEs you can match against their SBOM do not apply.

Consumed properly, that is useful. But it is somebody else's assertion about their product entering your gate, so it should arrive as a suppression carrying its provenance, naming who asserted it, under what identity and with what justification, and never as a finding that fails to appear. The difference matters for exactly the reason audit evidence does: an absence is not an answer.

The other answer is reachability: does your code call the vulnerable function? Commercial SCA does this for language dependencies. For operating-system packages inside an image it is much harder, and mostly not available. Anyone claiming to have solved it for the whole userland is worth questioning closely.

What to do on Monday#

  1. Separate image findings from code findings before you look at either. They have different owners and different fixes, and merging them into one list guarantees the fifteen that are yours get buried under the 219 that are not.
  2. Treat the base image as a dependency with a version. Because it is one. Update it on a schedule, not when a scan gets loud.
  3. Rank by exposure, not by count. 219 findings on an internal batch job and 219 on your public gateway are not the same problem, and no scanner can tell them apart, that context is not in the image.
  4. Record what you accept. The no-fix-available ones will still be there next quarter. If nobody wrote down that they were considered, somebody will consider them again from scratch.

Draugr scans images through Trivy as its images control, keeps them as a separate control from sca for exactly the reason above, and ranks everything by the exposure and criticality your descriptor declares. It also acts on the distinction this post is about: mark an image builtBy: upstream and its findings stop being presented as libraries for you to upgrade, and become one thing you can do, which is take a newer image. The same declaration works on a repository you consume and do not maintain, and on a whole component that is somebody else's software from top to bottom. The packages your own build installed stay listed as yours, because those you can fix. The container image scanning guide covers what the scanners see and how; the numbers at the top of this post came from the demo sandbox, which you can re-run.