Sign in
← Linux: security
11 min · medium · Lesson 4 of 5

Scanning images for vulnerabilities

Where vulnerabilities in images come from, how scanners work, and why a scanner is a process and one signal among others, not a verdict.

Where vulnerabilities in images come from, how a scanner works, how to reduce the number of findings, and why a clean scanner report still doesn't mean "secure".

Why this matters

You build an image out of other people's parts: a base image, packages from apk or apt, application libraries. Any one of them can have a known vulnerability. If you miss it before publishing, the image ships to prod with the hole still in it.

An image scanner finds these spots automatically. You wire it into CI, and an image with a critical finding doesn't ship any further.

Concepts

CVE (Common Vulnerabilities and Exposures) is a public identifier for a known vulnerability, for example CVE-2023-0464. The CVE Program maintains the catalog, and the number is the same across all databases and reports.

Base image is the image from the FROM line in a Dockerfile. Everything in it ends up in your image, whether you thought about it or not.

Package database is the file where the package manager records installed packages and their versions. On Alpine it's /lib/apk/db/installed, on Debian and Ubuntu it's /var/lib/dpkg/status.

Severity is the danger rating of a finding: LOW, MEDIUM, HIGH, CRITICAL.

Where vulnerabilities in an image come from

An image from the Docker lesson is made of a base layer, packages you installed, and application dependencies. A vulnerability can come from any of these parts: an old version of openssl in the base image, a library with a hole in the dependencies.

Packages get stale even without you doing anything. The alpine:3.20.3 image was built once and doesn't change after that. But fixes have shipped to the Alpine 3.20 repository in the meantime:

docker run --rm alpine:3.20.3 sh -c 'apk update >/dev/null; apk upgrade --simulate'
...
(5/10) Upgrading libcrypto3 (3.3.2-r0 -> 3.3.7-r0)
(6/10) Upgrading libssl3 (3.3.2-r0 -> 3.3.7-r0)
...
(8/10) Upgrading zlib (1.3.1-r1 -> 1.3.2-r0)
...

A tag with an exact version protects you from sudden changes, but it doesn't update itself. You have to remember to update the base image separately.

Here's another trap: a Dockerfile command pulls in dependencies. The line RUN apk add curl installs not one package but curl together with the libraries it needs. Each of them can also end up in the scanner's report.

What the scanner does

A scanner walks the image layer by layer and builds a list of packages with versions. For OS packages, it reads the package database. The list is checked against vulnerability databases, and the output is a set of findings with a severity level.

You can see the same thing the scanner sees in Alpine by hand:

docker run --rm alpine:3.20.3 apk info -v 2>/dev/null

2>/dev/null hides WARNING: opening from cache ... warnings: the image has no downloaded repository index, which doesn't affect the package list.

alpine-baselayout-3.6.5-r0
...
libcrypto3-3.3.2-r0
libssl3-3.3.2-r0
musl-1.2.5-r0
...
zlib-1.3.1-r1

This kind of package list is called an SBOM (Software Bill of Materials). The scanner builds it itself, then looks up known CVEs for every line.

There are plenty of tools: Trivy, Grype, Clair, and others. The idea behind them is the same, but the vulnerability databases and matching rules differ, so reports on the same image won't match one to one.

An example with Trivy (per the docs):

trivy image --severity HIGH,CRITICAL --exit-code 1 registry.example.com/app:1.4.0

--severity keeps only dangerous findings in the report. --exit-code 1 makes the command return a non-zero code if any were found. By default Trivy exits with code 0 even when there are findings, and the CI stage running it never fails. How pipeline stages work is covered in the CI lesson.

How to reduce the number of findings

Less content in the image means fewer potential holes.

Go shows how noticeable this is. The golang:1.23.3-alpine image weighs 244 MB, and almost all of it is Go itself in /usr/local/go. The program doesn't need it to run: after a multi-stage build on top of alpine:3.20.3, the same image weighs about 11 MB.

The scanner is one signal among several

A scanner is useful, but you can't trust it blindly.

So scanning is a process: regular, with fresh databases, with findings triaged. A one-off "we ran the tool" checkbox doesn't get you far.

Common mistakes

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson linux-security-04-image-scanning