Scan images with Trivy

Report everything, fail only on criticals.

Advanced12 min · lesson 20 of 24

Trivy is the default image scanner for good reasons: a single static binary, no server, and one pass that covers OS packages, language lockfiles, known misconfigurations, and even leaked secrets in the layers. The point of scanning in CI rather than in the registry is the feedback loop — a HIGH surfaced in the merge request is a ten-minute fix; the same HIGH found in production is a change ticket, a rollout, and a postmortem line item.

terminal
$ trivy image registry.internal/payments-api:1.4.2
payments-api:1.4.2 (debian 12.5)
Total: 23 (CRITICAL: 0, HIGH: 2, MEDIUM: 7, LOW: 14)
┌───────────────┬────────────────┬──────────┬───────────────────┐
│ Library │ Vulnerability │ Severity │ Fixed Version │
├───────────────┼────────────────┼──────────┼───────────────────┤
│ libssl3 │ CVE-2024-xxxx │ HIGH │ 3.0.13-1~deb12u1 │

Fail on what matters, not everything

A scanner that blocks every merge gets disabled within a month, so run two passes: the first reports the full picture with exit code 0 (nothing fails), the second gates the build with exit code 1 on only the severities you would actually page for. Add --ignore-unfixed so you are not blocking on CVEs with no available patch, which developers cannot act on anyway.

.gitlab-ci.yml
container_scan:
image: { name: aquasec/trivy:0.53.0, entrypoint: [""] } # pin the scanner
script:
- trivy image --exit-code 0 --severity LOW,MEDIUM,HIGH "$IMAGE"
- trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed "$IMAGE"

Scan what is already running

Beyond CI, point Trivy at the images a cluster is actually running — pull the image list straight from the pods, scan each, and read the totals to triage. It is the fast answer to “what are we running right now that is vulnerable,” which is exactly the question a new zero-day forces. Cache the vulnerability DB (about 40 MB) so repeated scans stay quick.

terminal
$ kubectl get pods -A \
-o custom-columns="IMAGE:.spec.containers[*].image" --no-headers \
| tr -s ' ' '\n' | sort -u \
| while read img; do echo "== $img"; trivy image -q -s HIGH,CRITICAL "$img"; done
Pin the scanner too
A floating aquasec/trivy:latest is exactly the supply-chain problem you are here to prevent — a scanner that can change under you is not a reproducible gate. Pin the scanner version, and clear the image entrypoint ([""]) in GitLab so your script lines run in a shell rather than being passed to the trivy binary.