Most of your container CVEs aren't yours
On this page
A repository built to be scanned. The application is 35 lines of Python. Scanning it produces 292 findings, and three quarters of them come from the container image. The code somebody actually wrote, reviewed and is responsible for accounts for fifteen.
Those 219 are real, published CVEs in real packages. 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, and not one is
fixable by changing the application.
Why this shape is normal
A container image is an operating system with your code on top, and the ratio is not close: a slim Python base is several hundred packages and your service is one. So "the image has 219 vulnerabilities" is mostly a report on a Linux distribution somebody else assembled and you chose in one line of a Dockerfile.
This is why container scanning has a reputation for noise it does not deserve. The findings are accurate. What is wrong is the implied call to action, because 219 CVEs sorted by severity reads as 219 pieces of work and it is one decision plus a short list.
The move that changes the number
Not triage, but a different base. It is the one action that moves this number by an order of magnitude rather than a few percent, and the mechanism is boring: fewer packages means fewer things to be vulnerable, and a maintained tag means the ones left are patched. In order of how much they remove:
| The move | What it removes, and what it costs |
|---|---|
| Update the tag you already use | python:3.8-slim is old, and much of that 219 is the passage of time. A newer patch tag of the same family removes a large share for one line and a test run |
| Go smaller | distroless and minimal-by-design images drop the shell, the package manager and everything else nobody needs in production |
| Build from a maintained minimal base | distributions built to carry near-zero known CVEs exist, and are worth evaluating on their own terms rather than because a vendor said so |
The lever is one line of the Dockerfile, 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.
| What is left | Whose problem it is |
|---|---|
| Packages you installed | yours. They behave like dependency findings because that is what they are |
| Base packages with no fix | nobody's yet. Decide, record it, revisit: risk acceptance rather than triage. Some publishers ship a VEX document saying which of these never applied to their image, which is the same decision made once by the people who built the thing |
| Code that never runs | the certificate bundle you do not parse, the compression format you never decompress. Present, real, and irrelevant to your image |
That last row is the one nobody can close for you. Reachability analysis answers it for language dependencies and, for the operating-system packages inside an image, mostly does not exist yet. Anyone claiming to have solved it across a whole Linux userland is worth questioning closely.
That third category is the one the industry is still working out how to express, and it has two answers worth knowing.
What to do on Monday
| Do this | Because | |
|---|---|---|
| 1 | Separate image findings from code findings before you look at either | different owners, different fixes, and merging them buries the fifteen that are yours under the 219 that are not |
| 2 | Treat the base image as a dependency with a version | it is one. Update it on a schedule rather than when a scan gets loud |
| 3 | Rank by exposure, not by count | 219 on an internal batch job and 219 on your public gateway are not the same problem, and that context is not in the image |
| 4 | Record what you accept | the no-fix ones will still be there next quarter, and if nobody wrote down that they were considered, somebody considers them again from scratch |
Draugr keeps images as its own control for exactly this reason, and acts on the distinction: mark
an image builtBy: upstream and its 219 findings stop arriving as 219 libraries to upgrade and
become the one thing you can actually do, which is take a newer image. Packages your own build
installed stay listed as yours, because those you can fix.
The container image scanning guide covers what the scanners see. The numbers above came from the demo sandbox, which you can re-run.