Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive · Docker

Scanning Images for Vulnerabilities with Trivy

Scan container images and file systems with Trivy, fail CI builds on serious findings, handle unfixed and accepted risks, and keep the scan database fresh.

4 min read

  • Trivy 0.14
  • Docker Engine 20.10.2
  • alpine:3.12
  • GitHub Actions

Every image contains an operating system layer and libraries, and some of those packages have known vulnerabilities (CVEs). A scanner compares what is inside an image with public vulnerability databases, so you find out before an attacker does. Trivy, an open-source scanner from Aqua Security, is simple enough to run on a laptop and in CI with no server to maintain.

Versions in use on this date. Trivy 0.14 and Docker Engine 20.10.2 (released 2 January 2021). Trivy reads OS packages (Alpine, Debian, Ubuntu, Red Hat, Amazon Linux and others) and application dependencies (npm, pip, Maven, Go modules and more) from lock files inside the image.

Where scanning fits

flowchart TB
  DEV[Developer commit] --> CI[CI build]
  CI --> IMG[Build image with Docker]
  IMG --> SC{Trivy scan<br/>HIGH and CRITICAL with fixes}
  SC -->|found| FAIL[Fail the build<br/>report CVE list]
  SC -->|clean| PUSH[Push to registry]
  PUSH --> DEPLOY[Deploy]
  REG[Registry] --> NIGHT[Scheduled nightly rescan<br/>of images in production]
  NIGHT -->|new CVE published| TICKET[Open ticket to rebuild]
  DB[(Vulnerability database<br/>updated several times a day)] -.-> SC
  DB -.-> NIGHT

A clean scan today does not stay clean: new CVEs are published daily against software you already shipped, which is why scheduled rescans matter as much as build-time scans.

Install and first scan

# Debian/Ubuntu package from the Aqua repository, or use the container
sudo apt install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt update && sudo apt install -y trivy

trivy image alpine:3.12
trivy image --severity HIGH,CRITICAL node:12-alpine

Or without installing anything:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache:/root/.cache \
  aquasec/trivy:0.14.0 image --severity HIGH,CRITICAL myapp:1.0.0

The first run downloads the vulnerability database (tens of MB); mounting a cache directory avoids repeating it. The output lists package, installed version, fixed version, severity and a CVE link.

Useful options

Option Purpose
--severity HIGH,CRITICAL Report only what you will act on
--ignore-unfixed Skip CVEs with no patch available yet
--exit-code 1 Return failure when findings remain, so CI stops
--format json -o report.json Machine-readable output
--vuln-type os,library Choose OS packages, application libraries, or both
--skip-update Use the cached database (offline or rate-limited runs)
trivy fs . Scan a project directory and its lock files before building
trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed myapp:1.0.0
echo $?     # 1 means the gate failed

Handling findings sensibly

  1. Fix by rebuilding: most OS CVEs disappear when you rebuild on a newer base image (FROM node:12-alpine pulls current patches only if you actually pull again). Rebuild with docker build --pull.

  2. Update the library shown in the “fixed version” column, then regenerate the lock file.

  3. Switch to a smaller base (Alpine, slim or distroless images) to remove packages you never use.

  4. Accept risk deliberately. For a CVE that is not exploitable in your context, list it in .trivyignore, one ID per line, with a comment explaining why and a review date:

    # CVE-2020-XXXXX  not reachable: we do not use the affected module. Review 2021-04-01
    CVE-2020-XXXXX
  5. Do not treat the severity as the full story: check whether the vulnerable code runs in your service, whether it is network-reachable, and whether a fix exists.

GitHub Actions example

name: build
on: [push]
jobs:
  image:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2
      - run: docker build -t myapp:${{ github.sha }} .
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          severity: CRITICAL,HIGH
          ignore-unfixed: true
          exit-code: '1'

Pin the action to a released version tag rather than master once you settle on one. A scheduled workflow (on: schedule) scanning the images that are currently deployed gives you the nightly rescan from the diagram.

Limits to remember

Scanners find known CVEs in recognised packages. They do not find bugs in your own code, secrets baked into layers, bad configuration or software installed from tarballs without a package record. Combine scanning with minimal images, non-root users, a read-only filesystem and secret scanning (for example in Git hooks). Record scan results (pipeline artifacts) so you can answer “were we affected?” when a new CVE hits the news.