| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | trivy 0.72.0podman 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.
Install and verify Trivy
Pin the scanner version in CI and on workstations. This lab uses Trivy 0.72.0:
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/trivyConfirm 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:
trivy image --download-db-only
trivy -vVersion: 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 UTCRecord these fields in every CI artifact bundle:
- Scanner version (
0.72.0) - Database
UpdatedAtandDownloadedAttimestamps - 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:
systemctl --user enable --now podman.socket
trivy image \
--image-src podman \
--scanners vuln \
localhost/trivy-demo:bloatedReport 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.
mkdir -p ~/trivy-lab && cd ~/trivy-lab
podman save localhost/trivy-demo:bloated -o bloated.tarRecord the immutable digest before you scan:
podman inspect localhost/trivy-demo:bloated --format '{{.Digest}}'sha256:3dcb33da34eb0e1dc247da92bbac6114441b349c45292b0e65da8467e1cba300Store 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:
trivy image --input bloated.tar --scanners vulnTrivy 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:
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:
trivy image --scanners vuln registry.k8s.io/e2e-test-images/busybox:1.29-4Report 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:
trivy image --scanners vuln alpine:3.12Report 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:
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"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:
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 withdnf,apt, orapk. - 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:
trivy image --input bloated.tar --scanners vuln --severity HIGH,CRITICALTotal: 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:
trivy image --input bloated.tar --scanners vuln --severity HIGH,CRITICAL --ignore-unfixedOn 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.
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:
trivy image --input bloated.tar --scanners vuln --severity HIGH,CRITICAL --exit-code 1
echo exit=$?exit=1The 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:
# 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}}'sha256:962b767a4cef7d478e58a849b98473e724e8e8ac32e07f4b8a5eee75986c2e73podman save localhost/trivy-demo:minimal -o minimal.tar
trivy image \
--input minimal.tar \
--scanners vuln \
--severity HIGH,CRITICAL \
--exit-code 1
echo exit=$?Report Summary
┌────────────────────────────┬──────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├────────────────────────────┼──────────┼─────────────────┤
│ minimal.tar (debian 12.15) │ debian │ 0 │
├────────────────────────────┼──────────┼─────────────────┤
│ server │ gobinary │ 0 │
└────────────────────────────┴──────────┴─────────────────┘
exit=0Because 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:
# .trivyignore.yaml — example governance entry
vulnerabilities:
- id: CVE-2026-45186
expired_at: 2026-12-31
statement: Vendor fix scheduled for the next image rebuildTrivy 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:
trivy image \
--input bloated.tar \
--scanners vuln \
--severity HIGH,CRITICAL \
--ignorefile .trivyignore.yamlAvoid 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:
trivy image --input bloated.tar --scanners vuln -f json --severity HIGH,CRITICAL -o bloated-high.jsonThe 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 -vscanner version- Database
UpdatedAttimestamp - Severity filter and
--ignore-unfixedflag state
SARIF
SARIF integrates with GitHub Advanced Security, Azure DevOps, and other SARIF consumers:
trivy image --input bloated.tar --scanners vuln -f sarif --severity HIGH,CRITICAL -o bloated-high.sarifUpload 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:
- 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.
- Remove development packages from the final stage; copy only the application binary into distroless or another minimal runtime.
- 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. - 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
- Generate and Scan Container SBOMs with Trivy
- Sign and Verify Container Images with Cosign
- Allow Trusted Images with ImagePolicyWebhook
References
- Trivy container image scanning
- Trivy vulnerability filtering
- Trivy report formats
- SARIF specification
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.

