Scan Container Images for Vulnerabilities with Trivy

Scan container images with Trivy, filter vulnerabilities by severity, enforce CI exit codes, manage ignores, export reports, and verify rebuilt images.

Published

Updated

Read time 11 min read

Reviewed byDeepak Prasad

Trivy vulnerability scan of a container image with severity filters, CI exit codes, and JSON and SARIF reports
Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package trivy 0.72.0
podman 5.8.2
Applies to Linux x86_64 systems with Podman or Docker
Cert prep CKS
Lab environment Workstation with Podman or Docker — reuse images from build secure minimal container images or export tarballs with podman save
Privilege Normal user for image scans; sudo to install the Trivy binary under /usr/local/bin
Scope Install and verify Trivy, scan local tarball and registry images, interpret OS versus language findings, filter severity, show fixable-only output, configure CI exit codes, govern .trivyignore.yaml, export JSON and SARIF, rebuild the application image on a minimal base, and compare reports by digest. Does not cover SBOM lifecycle, manifest scanning, registry administration, runtime detection, or full risk methodology.
Related guides Build and push container images
Verify Kubernetes release artifacts
Kubernetes security architecture

Trivy is a practical first gate before you promote a container image. This walkthrough installs a pinned release on Rocky Linux 10.2, scans a deliberately bloated local image and registry images, configures severity and fixable-only filters, demonstrates CI pass and fail exit codes, exports JSON and SARIF, and rescans after you rebuild the application image on a distroless runtime under a new digest.

IMPORTANT
This guide covers image vulnerability scanning with Trivy. It does not teach SBOM generation, Kubernetes manifest scanning, or Cosign signing. Pair scan results with secure image builds and promotion checks in your pipeline.

Install and verify Trivy

Pin the scanner version in CI and on workstations. This lab uses Trivy 0.72.0:

bash
TRIVY_VERSION=0.72.0
curl -sfL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" -o /tmp/trivy.tar.gz
tar xzf /tmp/trivy.tar.gz -C /tmp trivy
sudo mv /tmp/trivy /usr/local/bin/trivy

Confirm the binary and vulnerability database metadata before you scan release images. A new Trivy installation contains the scanner but not the vulnerability database; --download-db-only populates the cache without scanning an image:

bash
trivy image --download-db-only
trivy -v
output
Version: 0.72.0
Vulnerability DB:
  Version: 2
  UpdatedAt: 2026-07-28 07:38:05.767176531 +0000 UTC
  NextUpdate: 2026-07-29 07:38:05.767176291 +0000 UTC
  DownloadedAt: 2026-07-28 11:13:51.129272324 +0000 UTC

Record these fields in every CI artifact bundle:

  • Scanner version (0.72.0)
  • Database UpdatedAt and DownloadedAt timestamps
  • Cache path (default ~/.cache/trivy)

Trivy downloads the vulnerability database on first scan. Air-gapped environments should mirror that database separately; this article assumes outbound access to the default mirrors.


Prepare lab images

This article reuses localhost/trivy-demo:bloated, an 820 MB Rocky-based image with a compiled Go server and development packages left in the final stage. The remediation target is localhost/trivy-demo:minimal, a multi-stage rebuild that copies only the Go binary into distroless—the same pattern as the secure Dockerfile walkthrough.

The image was built in the current user's rootless Podman storage, but the user Podman API socket was not active. When the socket is available, scan directly:

bash
systemctl --user enable --now podman.socket

trivy image \
  --image-src podman \
  --scanners vuln \
  localhost/trivy-demo:bloated
output
Report Summary

┌───────────────────────────────────────────┬──────────┬─────────────────┐
│                  Target                   │   Type   │ Vulnerabilities │
├───────────────────────────────────────────┼──────────┼─────────────────┤
│ localhost/trivy-demo:bloated (rocky 10.2) │  rocky   │       48        │
├───────────────────────────────────────────┼──────────┼─────────────────┤
│ server                                    │ gobinary │        0        │
└───────────────────────────────────────────┴──────────┴─────────────────┘

When the socket is unavailable, export the image and scan the tar archive. Trivy officially supports Podman sockets, --podman-host, and Podman-generated tar files:

Archive extraction steps use the tar command where noted.

bash
mkdir -p ~/trivy-lab && cd ~/trivy-lab
podman save localhost/trivy-demo:bloated -o bloated.tar

Record the immutable digest before you scan:

bash
podman inspect localhost/trivy-demo:bloated --format '{{.Digest}}'
output
sha256:3dcb33da34eb0e1dc247da92bbac6114441b349c45292b0e65da8467e1cba300

Store digest, scanner version, and database timestamp together. Tags such as bloated can move; digests cannot.


Scan a local image

Run a default table scan against the exported tarball:

bash
trivy image --input bloated.tar --scanners vuln

Trivy prints a summary table, then per-target sections. Read each column as follows:

Column Meaning
Library OS package name or language artifact
Vulnerability CVE or vendor advisory ID
Severity CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN
Status fixed when a vendor fix exists in the database
Installed Version Package version inside the image layer
Fixed Version Version that contains the patch, when known
Title Short description and AVD/NVD link

The summary for the bloated image shows OS packages and a Go binary target:

output
Report Summary

┌──────────────────────────┬──────────┬─────────────────┐
│          Target          │   Type   │ Vulnerabilities │
├──────────────────────────┼──────────┼─────────────────┤
│ bloated.tar (rocky 10.2) │  rocky   │       48        │
├──────────────────────────┼──────────┼─────────────────┤
│ server                   │ gobinary │        0        │
└──────────────────────────┴──────────┴─────────────────┘

The OS scanner reported 48 vulnerability findings in Rocky Linux packages. One package can match multiple CVEs, so the summary's vulnerability count is not necessarily the number of affected packages. The Go binary scanner reported 0 language findings for this static binary.


Scan a registry image

Trivy can pull and scan remote references when the registry is reachable. This lab scans a public test image from registry.k8s.io without Docker Hub credentials:

bash
trivy image --scanners vuln registry.k8s.io/e2e-test-images/busybox:1.29-4
output
Report Summary

┌────────┬──────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├────────┼──────┼─────────────────┤
│   -    │  -   │        -        │
└────────┴──────┴─────────────────┘

Trivy did not identify a supported OS or package target in this BusyBox image. The dashes do not represent a clean vulnerability scan; they mean no vulnerability target was scanned.

For a useful registry demonstration, scan an image Trivy can classify and count:

bash
trivy image --scanners vuln alpine:3.12
output
Report Summary

┌──────────────────────────────┬────────┬─────────────────┐
│            Target            │  Type  │ Vulnerabilities │
├──────────────────────────────┼────────┼─────────────────┤
│ alpine:3.12 (alpine 3.12.12) │ alpine │        1        │
└──────────────────────────────┴────────┴─────────────────┘

That does not prove every registry image is vulnerable; it only shows Trivy identified an Alpine target and reported a numeric count at scan time.

For release decisions, scan by digest after you resolve the tag:

bash
podman pull docker.io/library/alpine:3.12

ALPINE_IMAGE=$(
  podman image inspect docker.io/library/alpine:3.12 \
    --format '{{index .RepoDigests 0}}'
)

printf '%s\n' "$ALPINE_IMAGE"
trivy image --scanners vuln "$ALPINE_IMAGE"
output
docker.io/library/alpine@sha256:c75ac27b49326926b803b9ed43bf088bc220d22556de1bc5f72d742c91398f69

Report Summary

┌──────────────────────────────────────────────────────────────────────────────────┬────────┬─────────────────┐
│                                      Target                                      │  Type  │ Vulnerabilities │
├──────────────────────────────────────────────────────────────────────────────────┼────────┼─────────────────┤
│ docker.io/library/alpine@sha256:c75ac27b49326926b803b9ed43bf088bc220d22556de1bc- │ alpine │        1        │
│ 5f72d742c91398f69 (alpine 3.12.12)                                               │        │                 │
└──────────────────────────────────────────────────────────────────────────────────┴────────┴─────────────────┘

Trivy distinguishes - from 0: - means the scanner did not scan that target, while 0 means it scanned successfully and found no matching issues.

Private registries use the same command after you authenticate with trivy registry login, or provide credentials through the supported Trivy environment variables in CI:

bash
printf '%s' "$REGISTRY_PASSWORD" |
  trivy registry login \
    --username "$REGISTRY_USERNAME" \
    --password-stdin registry.example.com

trivy image registry.example.com/team/app@sha256:...

Trivy stores registry-specific credentials through its own login command; TRIVY_USERNAME and TRIVY_PASSWORD are also supported.


Distinguish OS and language findings

Trivy runs separate scanners per layer type:

  • OS packages (rocky, debian, alpine, and similar) come from the base image and packages you install with dnf, apt, or apk.
  • Language packages (gobinary, python-pkg, node-pkg, jar, and others) come from application dependencies embedded in the image.

Remediation ownership differs:

Finding source Typical owner Fix path
OS package in final image Platform or image maintainer Rebuild from an updated final-stage base; update or remove packages installed in the final stage
Language dependency Application team Upgrade the dependency in the builder and rebuild the application artifact
Unnecessary package in final image Image maintainer Remove it or move build tooling into a separate builder stage

A clean language scan does not excuse vulnerable OS packages in the same image, and vice versa.


Filter severity

Limit the table to severities your policy treats as blocking:

bash
trivy image --input bloated.tar --scanners vuln --severity HIGH,CRITICAL
output
Total: 34 (HIGH: 34, CRITICAL: 0)

Your organization decides whether CRITICAL alone blocks promotion or whether HIGH is included. Document the threshold in the pipeline README, not only in the shell alias you use locally.


Show only fixable findings

Hide advisories with no published vendor fix:

bash
trivy image --input bloated.tar --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed

On this image the fixable HIGH count matched the full HIGH count because Rocky Linux advisories included fixed versions for the reported packages. --ignore-unfixed changes reporting, not the packages inside the image.

Unfixed rows still matter for risk tracking. Use a separate non-blocking job if security wants visibility into issues without vendor patches.


Configure CI exit codes

By default, vulnerability findings do not change Trivy's exit code. The command normally returns 0 when the scan completes, but image-pull, database, configuration, or other operational errors can still return a non-zero status. --exit-code changes the result when matching security findings are detected; it does not override unrelated execution failures.

bash
trivy image --input bloated.tar --scanners vuln --severity HIGH,CRITICAL
echo exit=$?

Blocking scans add --exit-code 1 so the job fails when matching severities exist:

bash
trivy image --input bloated.tar --scanners vuln --severity HIGH,CRITICAL --exit-code 1
echo exit=$?
output
exit=1

The bloated image fails the gate because 34 HIGH findings matched the filter.

After you rebuild the application image on a minimal runtime, the same command should pass:

bash
# Run from the directory containing the secure multi-stage Containerfile
podman build -t localhost/trivy-demo:minimal .

podman image inspect \
  localhost/trivy-demo:minimal \
  --format '{{.Digest}}'
output
sha256:962b767a4cef7d478e58a849b98473e724e8e8ac32e07f4b8a5eee75986c2e73
bash
podman save localhost/trivy-demo:minimal -o minimal.tar

trivy image \
  --input minimal.tar \
  --scanners vuln \
  --severity HIGH,CRITICAL \
  --exit-code 1

echo exit=$?
output
Report Summary

┌────────────────────────────┬──────────┬─────────────────┐
│           Target           │   Type   │ Vulnerabilities │
├────────────────────────────┼──────────┼─────────────────┤
│ minimal.tar (debian 12.15) │  debian  │        0        │
├────────────────────────────┼──────────┼─────────────────┤
│ server                     │ gobinary │        0        │
└────────────────────────────┴──────────┴─────────────────┘

exit=0

Because this command filters for HIGH,CRITICAL, zero means no findings matched those severities. Run another scan without --severity before claiming that the image has no findings at any severity.

This scan covers the rebuilt application image—including the Go binary inside distroless—not just an untouched base image pulled from a registry.

Pin TRIVY_VERSION in CI, pass the same severity flags, and archive stdout plus the JSON or SARIF file on every run.


Manage ignore policy

Use .trivyignore.yaml for time-bound exceptions, not permanent blind spots. Each entry should name the CVE, owner, reason, expiration, and compensating control in your ticket system:

yaml
# .trivyignore.yaml — example governance entry
vulnerabilities:
  - id: CVE-2026-45186
    expired_at: 2026-12-31
    statement: Vendor fix scheduled for the next image rebuild

Trivy ignores comments in plain .trivyignore files and will continue suppressing a CVE until the ID is removed. The YAML format supports expired_at, statement, package URLs, and path-specific exceptions.

Point Trivy at the file:

bash
trivy image \
  --input bloated.tar \
  --scanners vuln \
  --severity HIGH,CRITICAL \
  --ignorefile .trivyignore.yaml

Avoid blanket package or severity exclusions. Review ignored CVEs on a schedule and remove entries when the rebuilt digest no longer contains the vulnerable package version.


Generate reports

Table output

The default table is ideal for human triage in CI logs.

JSON

Machine-readable output carries metadata for dashboards and policy engines:

bash
trivy image --input bloated.tar --scanners vuln -f json --severity HIGH,CRITICAL -o bloated-high.json

The file includes schema version, artifact name, and per-target vulnerability arrays. Attach these fields to your release record:

  • Image digest (sha256:3dcb33da… for the bloated image)
  • trivy -v scanner version
  • Database UpdatedAt timestamp
  • Severity filter and --ignore-unfixed flag state

SARIF

SARIF integrates with GitHub Advanced Security, Azure DevOps, and other SARIF consumers:

bash
trivy image --input bloated.tar --scanners vuln -f sarif --severity HIGH,CRITICAL -o bloated-high.sarif

Upload the SARIF file as a pipeline artifact alongside the JSON report.


Remediate and rescan

Vulnerability scanning does not patch images. Remediation is always a new build under a new digest:

  1. Update the final-stage base image digest. If the final stage installs OS packages, update or remove them there. Upgrade application dependencies in the builder before recompiling the application.
  2. Remove development packages from the final stage; copy only the application binary into distroless or another minimal runtime.
  3. Rebuild the application image (podman build -t localhost/trivy-demo:minimal .) and push with a new tag if needed, but gate promotion on the digest.
  4. Rescan the exact digest you plan to deploy.
Image Digest (lab) HIGH+CRITICAL at scan time CI --exit-code 1
trivy-demo:bloated sha256:3dcb33da… 34 fails (1)
trivy-demo:minimal sha256:962b767a4cef7d478e58a849b98473e724e8e8ac32e07f4b8a5eee75986c2e73 0 passes (0)

Do not rescan a mutable tag and assume the running cluster still uses the same bytes. Compare kubectl describe pod imageID or registry manifest digests against the scan report metadata.


Common misinterpretations

Mistake Why it is wrong
Tag moved after scan The report belongs to the digest you scanned, not the tag pointer today
Fixed version listed but image unchanged Status fixed means a vendor fix exists; you still must rebuild
--ignore-unfixed equals remediation It only hides unfixed rows from the report
Stale vulnerability database trivy -v shows UpdatedAt; fail CI when the DB is too old for your policy
Scanned local tarball but cluster pulls different mirror Scan the same reference the runtime will pull
SBOM export treated as patch SBOM lists components; it does not upgrade packages

Troubleshooting

Symptom Likely cause Fix
unable to find the specified image with Podman User Podman API socket not active Enable podman.socket, scan with --image-src podman, or podman save image -o image.tar and scan with --input
toomanyrequests pulling from Docker Hub Anonymous rate limit Authenticate, mirror images to your registry, or scan tarballs built locally
Empty report for old minimal image Scanner found no matching CVEs at current DB age Record DB timestamp; rescan after DB update
--exit-code 1 but table looks short Severity filter hides lower rows Run without --severity for full context
Go binary warnings about malformed versions Red Hat extended version strings OS findings still report; watch for scanner updates
CI passes locally, fails in pipeline Different Trivy version or DB age Pin version and cache or mirror the DB in CI

What's Next


References


Summary

Trivy turns image bytes into actionable vulnerability rows before deployment. Install a pinned release, record scanner and database versions, scan local tarballs or registry references by digest, and separate OS findings from language findings. Use --severity and --ignore-unfixed to match your policy, then wire --exit-code 1 when HIGH or CRITICAL rows should block promotion. Export JSON and SARIF with digest metadata for auditors and CI dashboards.

Remediation is always rebuild and rescan: the lab reduced reported HIGH and CRITICAL findings from 34 to 0 after rebuilding trivy-demo:minimal on the distroless runtime. Next steps include SBOM generation in container-image-sbom-trivy and signing promoted digests with Cosign before they reach production.


Frequently Asked Questions

1. Does Trivy scan only OS packages in a container image?

No. Trivy detects OS packages from the base distribution and language-specific dependencies such as Go binaries, Python wheels, and Node modules when they are present in image layers. Each result section identifies the OS or language target, while each finding row shows the affected package, installed version, fixed version, severity, and advisory ID.

2. What is the difference between --ignore-unfixed and fixing a vulnerability?

--ignore-unfixed hides findings that have no vendor fix published in the vulnerability database. The underlying package version in your image is unchanged. Remediation still requires rebuilding on an updated base, upgrading packages, or removing unused dependencies, then scanning the new digest.

3. Should CI fail on MEDIUM severity findings?

That is a policy decision, not a Trivy default. Many pipelines block on HIGH and CRITICAL only, while reporting MEDIUM and LOW for triage. Document the threshold in your pipeline and store the scanner version, database timestamp, and image digest with every report artifact.

4. Why does Trivy exit code 1 even when the table prints results?

When you pass --exit-code 1, Trivy returns a non-zero exit status if any finding matches your severity filter. Pipelines use that signal to fail a build. A reporting-only job omits --exit-code or sets --exit-code 0 so the same table prints without failing the job.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)