Trivy and Container Scanning: Finding Vulnerabilities Before They Find You

Scanning images, filesystems and IaC without selling a kidney

Every Docker image you pull is a tarball of someone else’s decisions. That base image you chose two years ago because the tutorial used it? It’s carrying an OpenSSL with a known hole, a libc with a CVE, and three system packages you’ve never heard of, one of which has a remote code execution bug filed against it. You didn’t write any of that. You’re still running it.

Container scanning is the unglamorous practice of finding out what’s actually inside your images before an attacker does. And the tool I reach for first, every time, is Trivy — partly because it’s genuinely good, and partly because it’s free, fast, and doesn’t try to drag me into a sales call.

Advertisement

Trivy reads through an image layer by layer, builds an inventory of every OS package and application dependency it can find, and cross-references that inventory against vulnerability databases. The output is a list of CVEs, each with a severity, the installed version, and — the bit that makes it useful rather than just frightening — the version that fixes it.

The single-command experience is the whole pitch:

trivy image nginx:1.25.2

That pulls the image (or reads it locally), enumerates the packages, downloads the latest vuln database, and prints a table. The first thing you learn is humbling: a “clean” official image off Docker Hub will routinely show dozens of vulnerabilities, most of them inherited from the base. Scanning isn’t about reaching zero. It’s about knowing your numbers and not shipping the ones that matter.

The trap people fall into is thinking Trivy is only a container thing. It scans far more than that, and this is where it earns a permanent spot in the toolbox:

  • Filesystems and Git repos — point it at a directory or a repo URL and it’ll find vulnerable dependencies in your package-lock.json, go.sum, requirements.txt and friends.
  • Infrastructure-as-code — it lints Terraform, Kubernetes manifests, Dockerfiles and Helm charts for misconfigurations, like a container running as root or a security group open to the world.
  • Secrets — it’ll flag an AWS key or private key that someone helpfully committed.
# scan your source tree for vulnerable deps AND leaked secrets
trivy fs --scanners vuln,secret,misconfig .

One tool, the same database plumbing, covering the image, the code that built it, and the manifests that deploy it. That breadth is why I stopped juggling three separate scanners.

A scanner you run manually is a scanner you forget. The win comes from making the pipeline fail when something genuinely dangerous turns up. The flag that makes this practical is --exit-code, paired with a severity floor so you’re not blocking every build over a low-severity issue in a package you’ll never call:

# .github/workflows/scan.yml (excerpt)
- name: Scan image
  run: |
    trivy image \
      --severity HIGH,CRITICAL \
      --ignore-unfixed \
      --exit-code 1 \
      myorg/myapp:${{ github.sha }}

Two flags there are doing the heavy lifting. --severity HIGH,CRITICAL keeps the signal high. --ignore-unfixed is the pragmatic one: there’s no point failing a build over a CVE that has no patch available yet — you can’t fix it, so blocking on it just trains people to slap continue-on-error everywhere and ignore the lot. Fail on what’s actionable.

Here’s the honest bit. Scanners are noisy. You will get findings for a vulnerable library that your code never actually calls, or a CVE that’s only exploitable in a configuration you don’t run. Treating every CRITICAL as a five-alarm fire is how teams burn out and start ignoring the scanner entirely — which is worse than never having run it.

The answer is a triage file. Trivy supports a .trivyignore listing the CVE IDs you’ve assessed and consciously accepted, ideally with a comment and an expiry date so the decision gets revisited:

# CVE-2023-XXXXX  vulnerable lib present but code path unreachable; review 2024-09
CVE-2023-XXXXX

The discipline isn’t “scan and panic.” It’s “scan, triage, fix what’s fixable, document what you’re accepting, and rebuild on a fresh base regularly so the inherited cruft ages out.” Most of your real wins come not from chasing individual CVEs but from moving to a slimmer base image — a -slim or distroless variant — which removes whole categories of vulnerable packages you were never using anyway.

If you ship containers and you aren’t scanning them, Trivy is the lowest-effort, highest-return change you can make this week. It’s a single binary, it runs offline once the database is cached, it covers images and code and IaC, and it costs nothing. Wire it into CI with a sensible severity floor and --ignore-unfixed, keep a triage file, and rebuild on slim bases.

Who is this for? Anyone running other people’s images in production — which is everyone. It won’t make you bulletproof; nothing does. But “we knew about it and chose to ship anyway” is a defensible position. “We had no idea what was in the image” is not.

Advertisement

Related Content

Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.