Trivy on Debian 13: Verified Install and Scans

13 min read

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 image scans a container image (local Docker, containerd, Podman, or a remote registry).
  • trivy fs scans a project directory (lockfiles, secrets, optionally IaC).
  • trivy rootfs scans a host or unpacked root filesystem, including OS packages.
  • trivy config scans IaC: Dockerfile, Kubernetes, Terraform, CloudFormation, ARM, Helm.
  • trivy sbom scans 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-action and aquasecurity/setup-trivy were 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 stateTrivy statusHidden by --ignore-unfixed
fixed (version listed)fixedno
no-dsa, unfixedaffectedyes
postponedfix_deferredyes
ignoredwill_not_fixyes
end-of-lifeend_of_lifeyes

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 | sh installers, :latest tags 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 --platform for arm64 images.
  • Remote Podman isn't supported. Scan locally or export with podman save and use trivy 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 COPY but 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

TaskCommand or flag
Verify releasecosign 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 packagestrivy rootfs --pkg-types os --scanners vuln /
IaCtrivy 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

Comments

Latest alerts