Trivy is Aqua Security's open-source scanner. One binary checks container images, directories, a host root filesystem and IaC files for known CVEs, leaked secrets and misconfigurations. In March 2026 the scanner itself became the attack: an attacker with stolen release credentials published a poisoned Trivy v0.69.4 that stole credentials. Later, Docker Hub images tagged 0.69.5 and 0.69.6 were poisoned too. This article covers installing Trivy on Debian 13 from a verified source, scanning images, filesystems and configs with it, and making sense of its Debian results without drowning in no-dsa noise.
What Trivy is for and when to use it
Use Trivy when you need to know which packages inside an image or on a host have known vulnerabilities. Typical cases are base images you pull from registries, images you build with Podman or Docker, application dependency trees in a repo, and Dockerfiles or Kubernetes manifests before they ship. It doesn't replace apt upgrade or security monitoring. It reads package metadata and lockfiles and compares them with a vulnerability database, so it only finds what that database knows about.
The subcommands you'll use most:
trivy imagescans a container image (local Docker, containerd, Podman, or a remote registry).trivy fsscans a project directory (lockfiles, secrets, optionally IaC).trivy rootfsscans a host or unpacked root filesystem, including OS packages.trivy configscans IaC: Dockerfile, Kubernetes, Terraform, CloudFormation, ARM, Helm.trivy sbomscans an existing SPDX or CycloneDX document.
Trust the scanner first: the March 2026 compromise
The advisory is GHSA-69fq-xp46-6x23 / CVE-2026-33634 (CWE-506, embedded malicious code). Here is the short version from Aqua's advisory and incident post:
- Late February 2026: attackers abused a misconfiguration in Trivy's GitHub Actions environment and got a privileged token. Credentials were rotated on 1 March, but Aqua later found that rotation “was not fully comprehensive”.
- 2026-03-19 18:22 UTC to about 21:42 UTC: malicious v0.69.4 binaries were on GitHub Releases, in the .deb and .rpm packages, and in images on GHCR, ECR Public and Docker Hub. Tags in
aquasecurity/trivy-actionandaquasecurity/setup-trivywere force-pushed to malware. - 2026-03-22 15:43 UTC to about 2026-03-23 01:40 UTC: Docker Hub images tagged 0.69.5 and 0.69.6 were malicious.
- Not affected: v0.69.3 and earlier, images referenced by digest, and binaries built from source.
The payload went after SSH keys, cloud credentials, Kubernetes tokens, Docker configs, .env files and database credentials. If any of those versions ran on a machine or in a pipeline, check it for IOCs and rotate every secret it could reach. Here are two archive hashes from the advisory (the full list there covers other platforms and image digests):
385d498d18a3a7c67878ca7322716f9da25683eb1a4bf9e9592da0d5f2ab09f6 trivy_0.69.4_Linux-64bit.tar.gz
0ca60dd18178d1c79d59cc06be12c540c121a4aea467484244667131aa13c311 trivy_0.69.4_Linux-64bit.deb
trivy --version
find / -xdev -name 'trivy_0.69.*' -exec sha256sum {} + 2>/dev/null
podman images --digests | grep -i trivy
grep -rs 'aquasecurtiy' /var/log/ | head
The last command looks for the lookalike exfiltration domain scan.aquasecurtiy.org (note the misspelling). Also block that domain and 45.148.10.212 at egress, and check your GitHub organisations for repositories named tpcp-docs- followed by a suffix. The malware used those as a fallback exfiltration channel.
Install Trivy on Debian 13 from a verified source
Trivy isn't packaged in Debian: packages.debian.org has no trivy in any suite. Your options are Aqua's APT repository, the release .deb or tarball, or the container image. At the time of writing, the latest release is v0.76.0 (14 October 2026).
The APT repo is GPG-signed, but that signature only proves Aqua's pipeline published the package. The pipeline itself was what got compromised, and the v0.69.4 .deb was among the bad artifacts. My preference: pin an exact version and verify its Sigstore bundle, which ties the file to the specific GitHub Actions release workflow and tag.
Debian 13 ships cosign 2.5.0. Trivy's docs assume cosign 3.x and say to add --new-bundle-format on cosign 2.4.0 or later:
sudo apt install cosign
mkdir -p ~/dl/trivy-0.76.0
cd ~/dl/trivy-0.76.0
V=0.76.0
BASE=https://github.com/aquasecurity/trivy/releases/download/v${V}
wget "${BASE}/trivy_${V}_Linux-64bit.tar.gz"
wget "${BASE}/trivy_${V}_Linux-64bit.tar.gz.sigstore.json"
wget "${BASE}/trivy_${V}_checksums.txt"
wget "${BASE}/trivy_${V}_checksums.txt.sigstore.json"
cosign verify-blob "trivy_${V}_Linux-64bit.tar.gz" --new-bundle-format --bundle "trivy_${V}_Linux-64bit.tar.gz.sigstore.json" --certificate-oidc-issuer=https://token.actions.githubusercontent.com --certificate-identity "https://github.com/aquasecurity/trivy/.github/workflows/reusable-release.yaml@refs/tags/v${V}"
cosign verify-blob "trivy_${V}_checksums.txt" --new-bundle-format --bundle "trivy_${V}_checksums.txt.sigstore.json" --certificate-oidc-issuer=https://token.actions.githubusercontent.com --certificate-identity "https://github.com/aquasecurity/trivy/.github/workflows/reusable-release.yaml@refs/tags/v${V}"
sha256sum -c --ignore-missing "trivy_${V}_checksums.txt"
Each verify-blob must print Verified OK and sha256sum must print OK. If either doesn't, don't install. Then:
tar -xzf trivy_0.76.0_Linux-64bit.tar.gz trivy
sudo install -o root -g root -m 0755 trivy /usr/local/bin/trivy
trivy --version
The same verify-blob works on the .deb. If you use the APT repository anyway, put the package on hold (sudo apt-mark hold trivy) and upgrade on purpose, after the release has been out for a while and you've checked its bundle. A malicious release that's live for three hours only reaches you if you pull it during those three hours.
For the container image, verify the signature and then reference it by digest. A tag can be moved; a digest can't:
cosign verify ghcr.io/aquasecurity/trivy:0.76.0 --certificate-identity-regexp 'https://github\.com/aquasecurity/trivy/\.github/workflows/.+' --certificate-oidc-issuer https://token.actions.githubusercontent.com
podman pull ghcr.io/aquasecurity/trivy:0.76.0
podman image inspect --format '{{.Digest}}' ghcr.io/aquasecurity/trivy:0.76.0
From then on, use ghcr.io/aquasecurity/trivy@sha256: followed by that digest in Quadlet units and CI. In GitHub Actions, pin trivy-action to a full commit SHA. Aqua's own post-incident advice is that SHA pinning “remains the only reliable protection”, since even the “Immutable” badge doesn't stop tag force-pushes.
Real usage: image, fs, rootfs and config
Scan an image for vulnerabilities and secrets, which are the default scanners, and show only HIGH and CRITICAL findings that have a fix:
trivy image --severity HIGH,CRITICAL --ignore-unfixed debian:trixie-slim
The table output groups findings by target (OS packages first, then each language lockfile). Each row has the library, vulnerability ID, severity, status, installed version and fixed version. The header line looks like this (counts depend on the day's database):
debian:trixie-slim (debian 13.x)
Total: N (HIGH: N, CRITICAL: N)
Trivy finds Podman images through the user socket, so enable it first. If you run rootless containers as described in rootless Podman Quadlet units on Debian 13, this is how you scan what those units actually run:
systemctl --user enable --now podman.socket
trivy image --image-src podman localhost/myapp:latest
Scan a project directory for vulnerable dependencies, secrets and misconfigurations. Misconfiguration scanning is off by default for image, fs and repo:
trivy fs --scanners vuln,secret,misconfig /srv/app
Scan the Debian host's own OS packages. With --pkg-types os Trivy skips the full directory walk:
trivy rootfs --pkg-types os --scanners vuln /
Check IaC only. A Dockerfile whose last USER is root triggers check DS002; one with an untagged FROM triggers DS001:
trivy config --severity HIGH,CRITICAL ./deploy
A misconfig scan won't catch a privileged container at runtime. For what an escape actually looks like, see how container escapes work through runc. Use the ksv012 (runAsNonRoot) and ksv001 (allowPrivilegeEscalation) checks to keep manifests out of that territory.
Reading Debian results: no-dsa and unfixed noise
On Debian images and hosts, most of the long CVE list is noise you can't act on. To know what you're hiding, you need to know how trivy-db maps Debian security tracker states. From pkg/vulnsrc/debian/debian.go:
| Debian tracker state | Trivy status | Hidden by --ignore-unfixed |
|---|---|---|
| fixed (version listed) | fixed | no |
no-dsa, unfixed | affected | yes |
postponed | fix_deferred | yes |
ignored | will_not_fix | yes |
end-of-life | end_of_life | yes |
The Debian security team uses no-dsa for lower-impact issues that don't justify an advisory. These issues often get fixed later in a point release. So “affected” on Debian usually means “known, triaged, waiting for a point release”, not “unknown exposure”. --ignore-unfixed is shorthand for --ignore-status affected,will_not_fix,fix_deferred,end_of_life. It leaves only findings you can fix with an upgrade or a rebuild.
Severity is the second trap. On Debian, Trivy takes severity from the tracker's urgency field and falls back to NVD only when Debian gives none. Debian's tracker documentation says urgency levels are no longer applied to current data, and only unimportant is still in use, so most recent CVEs get NVD's severity and can still show as CRITICAL. Where a Debian urgency is present (mostly older entries, plus anything marked unimportant), it overrides NVD. trivy-db maps it to LOW, MEDIUM or HIGH (unimportant becomes LOW) and never to CRITICAL. That means a CVE that's Critical in NVD can show up as LOW for a Debian package. A gate of --severity CRITICAL alone can miss those, so use HIGH,CRITICAL.
What works in practice is two tiers. Alert on fixed HIGH/CRITICAL findings, because those mean “rebuild or upgrade now”. Then review the affected list monthly with --ignore-status will_not_fix,end_of_life to see what's coming in the next point release.
Options that matter
.trivyignore with expiry
Accepted risks go in an ignore file. The default is .trivyignore in the working directory. Always set an expiry so suppressions don't live forever:
# CVE-YYYY-NNNNN: not reachable, binary not shipped - reviewed by pj
CVE-YYYY-NNNNN exp:2026-12-31
# Dockerfile runs as root by design in this build stage
DS002
Pass it with --ignorefile /etc/trivy/trivyignore. The YAML format (.trivyignore.yaml, with paths, purls and expired_at) is experimental and needs an explicit --ignorefile.
Exit codes for cron and CI
Trivy exits 0 even when it finds vulnerabilities. Set --exit-code and pick a value other than 1, so a crash or a network failure can't pass for findings. --exit-on-eol 1 also fails scans of an end-of-life OS.
DB caching and offline mode
The vulnerability DB is an OCI artifact pulled from mirror.gcr.io/aquasec/trivy-db, with ghcr.io/aquasecurity/trivy-db as the fallback. Set a fixed cache directory, download the DB once per run, and skip updates on the scans that follow:
sudo mkdir -p /var/cache/trivy
sudo trivy image --cache-dir /var/cache/trivy --download-db-only
sudo trivy rootfs --cache-dir /var/cache/trivy --skip-db-update --skip-java-db-update --offline-scan --pkg-types os --scanners vuln /
On air-gapped hosts, copy the cache directory from a connected machine. You can also mirror the DB into your own registry and point Trivy at it with --db-repository. That flag replaces the defaults, so list the official locations too if you want them as a fallback. --offline-scan stops lookups against Maven Central. The misconfiguration checks bundle is embedded in the binary as a fallback.
SBOM output
trivy image --format spdx-json --output myapp.spdx.json localhost/myapp:latest
trivy image --scanners vuln --format cyclonedx --output myapp.cdx.json localhost/myapp:latest
trivy sbom myapp.spdx.json
CycloneDX output leaves out vulnerabilities unless you add --scanners vuln. Keep SBOMs next to the image digest. When a new CVE lands, trivy sbom answers “are we affected?” without pulling images again.
A weekly scan with mail alerts
This script scans the host's OS packages and a list of images, and mails root only when there are fixable HIGH/CRITICAL findings or the scan breaks. It assumes a working local MTA and bsd-mailx. The setup is in DKIM, SPF and DMARC for Postfix on Debian 13.
#!/bin/bash
set -u
CACHE=/var/cache/trivy
LIST=/etc/trivy/images.txt
REPORT=$(mktemp)
FOUND=0
FAILED=0
common=(--cache-dir "$CACHE" --skip-db-update --quiet --severity HIGH,CRITICAL --ignore-unfixed --ignorefile /etc/trivy/trivyignore --exit-code 5)
check() {
local rc=$1
if [ "$rc" -eq 5 ]; then FOUND=1; elif [ "$rc" -ne 0 ]; then FAILED=1; fi
}
if ! trivy image --cache-dir "$CACHE" --quiet --download-db-only >>"$REPORT" 2>&1; then
FAILED=1
fi
echo "== host rootfs" >>"$REPORT"
trivy rootfs "${common[@]}" --pkg-types os --scanners vuln / >>"$REPORT" 2>&1
check $?
while read -r img; do
[[ -z "$img" || "$img" == \#* ]] && continue
echo "== $img" >>"$REPORT"
trivy image "${common[@]}" --image-src remote --scanners vuln,secret "$img" >>"$REPORT" 2>&1
check $?
done < "$LIST"
if [ "$FOUND" -eq 1 ] || [ "$FAILED" -eq 1 ]; then
mail -s "trivy $(hostname): found=$FOUND failed=$FAILED" root < "$REPORT"
fi
rm -f "$REPORT"
[Unit]
Description=Weekly Trivy vulnerability scan
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/trivy-weekly.sh
Nice=10
IOSchedulingClass=idle
[Unit]
Description=Run Trivy scan weekly
[Timer]
OnCalendar=Mon 05:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
sudo chmod 0750 /usr/local/sbin/trivy-weekly.sh
sudo systemctl daemon-reload
sudo systemctl enable --now trivy-weekly.timer
sudo systemctl start trivy-weekly.service
Remote image scans need registry credentials for private images. The scanner sees whatever the service user can read, so treat that host as sensitive. That's the same lesson the March incident taught. You can tighten the unit with the options from sandboxing systemd services, but keep / readable or the rootfs scan breaks.
Pitfalls, limitations and alternatives
- Unverified installs:
curl | shinstallers,:latesttags and unpinned Actions are exactly how the v0.69.4 payload spread. - A CRITICAL-only gate can miss Debian issues. Where a Debian urgency is set (mostly older entries and
unimportant), it overrides NVD and never maps to CRITICAL. - Without
--exit-code, cron and CI jobs pass silently, and exit code 1 can't tell findings apart from errors. - An ignore file without
exp:dates collects permanent blind spots. - Trivy assumes linux/amd64 for images. Set
--platformfor arm64 images. - Remote Podman isn't supported. Scan locally or export with
podman saveand usetrivy image --input. - A stale DB in offline mode gives a false sense of “clean”. Track how old the copied cache is.
- Statically linked binaries and software copied in with
COPYbut not installed through a package manager or lockfile are mostly invisible.
Alternatives: Grype from Anchore (grype alpine:latest, grype ./dir, grype sbom:./sbom.json, plus --from containers-storage for local Podman images) pairs with Syft for SBOMs and makes a good second opinion. With rootless Podman, run it through podman unshare so it can see your user's image store:
podman unshare grype --from containers-storage localhost/myimage:latest
Docker Scout is a Docker service. docker scout cves supports --only-fixed, --only-severity and -e (exit 2 on findings). It fits Docker Hub-centric shops and doesn't fit self-hosted Podman. Neither tool does Trivy's IaC checks.
Cheat sheet and takeaways
| Task | Command or flag |
|---|---|
| Verify release | cosign verify-blob with --new-bundle-format on trixie's cosign 2.5.0 |
| Fixable HIGH/CRITICAL only | --severity HIGH,CRITICAL plus --ignore-unfixed |
| All scanners | --scanners vuln,secret,misconfig |
| Host OS packages | trivy rootfs --pkg-types os --scanners vuln / |
| IaC | trivy config ./deploy |
| Fail CI | --exit-code 5, --exit-on-eol 1 |
| Offline | --download-db-only, then --skip-db-update and --offline-scan |
| SBOM | --format spdx-json or --format cyclonedx |
| Suppress | --ignorefile with exp: dates |
- Check for v0.69.4, and for Docker Hub 0.69.5 or 0.69.6, using the advisory's hashes and digests, and rotate secrets wherever they ran.
- Install a pinned release, verify its Sigstore bundle, and reference images by digest.
- On Debian, “affected” usually means no-dsa. Alert on fixed findings and review the rest monthly.
- Use explicit exit codes, a fixed cache directory and a weekly timer that only mails when something needs doing.
Sources
- GitHub Security Advisory GHSA-69fq-xp46-6x23: Trivy ecosystem supply chain temporarily compromised
- Aqua Security: Trivy supply chain attack – what you need to know
- Trivy releases on GitHub
- Trivy v0.76.0 release
- Trivy docs: Installation
- Trivy docs: Signature Verification
- Debian packages search: trivy
- Debian packages search: cosign
- Trivy docs: Filtering
- Trivy docs: Debian coverage
- trivy-db Debian source: debian.go
- Debian Security Team: Security Tracker documentation
- Trivy CLI reference: trivy image
- Trivy docs: Databases
- Trivy docs: Air-gapped environment
- Trivy docs: Cache
- Trivy docs: SBOM
- Trivy docs: Container image target
- Trivy docs: Root filesystem target
- Trivy docs: Misconfiguration scanning
- Trivy docs: Others (exit code, EOL)
- Anchore Grype on GitHub
- Docker docs: docker scout cves
Comments