How to scan Docker images with Trivy in GitHub Actions
Every container image I build gets scanned with Trivy before it goes anywhere, and signed with cosign after it passes. Adding the scan isn't hard. The work is deciding what fails the build, getting the fix in front of the developer, and making sure the scanner itself can be trusted. That last part got a lot of attention in March 2026.
The basic scan
Trivy scans an image for known vulnerabilities in its OS packages and language dependencies. In a workflow, the core of it is one command after the image is built:
trivy image \
--severity HIGH,CRITICAL \
--exit-code 1 \
--ignore-unfixed \
"registry.example.com/my-app@sha256:${DIGEST}"
--severitylimits which findings count.--exit-code 1fails the step when it finds something. That's what turns a report into a gate.--ignore-unfixedleaves out findings that have no fixed version yet, since the developer can't act on those today.
Scan the exact image you're about to ship, by digest, not by a tag that can change between the scan and the deploy.
As a workflow step, with the action pinned to a full commit SHA and the version in a comment so people can read it. Take the SHA from the release you've checked, not from this page.
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@<full-commit-sha> # v0.35.0
with:
image-ref: registry.example.com/my-app@sha256:<digest>
severity: HIGH,CRITICAL
exit-code: "1"
ignore-unfixed: true
Deciding what fails the build
If the first scan fails every build, people find a way around it. An order that keeps teams on board:
- Run the scan in report-only mode for a couple of weeks and look at what it finds.
- Fail on critical findings that have a fix. Then add high.
- Keep accepted exceptions in a
.trivyignorefile in the repository, with a comment on why and who accepted it, so they get reviewed like any other change.
Getting the fix to the developer
A failed build with a wall of CVE numbers mostly gets ignored. On the developer platform I built, the scan results show up in one place and the developer has an option to fix them: the platform updates the versions to the ones Trivy recommends. The developer reviews one change instead of hunting down each package by hand.
You don't need a platform to get most of the way there. Trivy reports the fixed version for each finding, which is the line the developer actually needs, and it can write results as SARIF for GitHub's code scanning if your plan includes it.
The March 2026 Trivy compromise
If you run Trivy in GitHub Actions, check this. According to GitHub advisory GHSA-69fq-xp46-6x23 (CVE-2026-33634), on March 19, 2026 an attacker used compromised credentials to publish a malicious Trivy v0.69.4 release, force-push 76 of 77 version tags in aquasecurity/trivy-action to credential-stealing malware, and replace all 7 tags in aquasecurity/setup-trivy with malicious commits. On March 22, 2026, malicious Trivy v0.69.5 and v0.69.6 images were published to Docker Hub.
The advisory's recommended actions, in short:
- Move to the known-safe versions it lists: the Trivy binary at v0.69.2 or v0.69.3, trivy-action at v0.35.0, setup-trivy at v0.2.6.
- If there's any chance a compromised version ran in your environment, treat every secret those pipelines could reach as exposed and rotate it.
- Check whether anything pulled or ran Trivy v0.69.4, and review workflow run logs from March 19 to 20, 2026. Also check for the malicious v0.69.5 and v0.69.6 images pulled from Docker Hub between March 22 and 23, 2026.
- Look for a repository named
tpcp-docsin your GitHub organization. The advisory says it may mean secrets were taken. - Pin GitHub Actions to full commit SHAs, not version tags.
Read the advisory itself for the exposure windows and exactly who was and wasn't affected. Go by the advisory, not by this summary.
Running a scanner you can trust
The lesson is bigger than one incident. The scanner runs inside your pipeline, next to your secrets, so it's part of your supply chain like everything else.
- Pin by SHA or digest. The advisory notes that Trivy images referenced by digest were not affected. A tag is a pointer somebody else can move. GitHub's guidance on third-party actions says the same.
- Run it from an image you control. Pull a verified version into your own registry and run it from there. On a self-hosted runner, that also means builds don't depend on reaching the internet.
- Verify before you trust. Trivy releases are signed, and the advisory shows how to verify a binary or an image with cosign.
- Mirror the vulnerability database if your network is locked down. Trivy can pull its database from your own registry with
--db-repository. - Give the scan step as little as possible. A scan doesn't need deploy credentials. Keep those in a later job.
The advisory's own check accepts a signature from any workflow in the aquasecurity organization, which is the organization that was compromised. Pin it tighter, to the exact release workflow named in the signing certificate. For v0.69.2 that workflow is the one below, taken from the signing certificate in that release's own signature bundle. Check it again for the version you use.
Get the digest from that check, not from pulling the tag. A successful cosign verify reports the digest of the image it verified. Pin and mirror by that digest, and run the scanner from your own registry. Save the block below as a script and run it. Don't paste it into a terminal, because it's set to stop at the first command that fails, and in an open terminal that closes the session.
#!/usr/bin/env bash
set -euo pipefail
# verify the release image against its exact release workflow,
# and keep the digest cosign reports for the image it verified
TRIVY_DIGEST=$(cosign verify \
--certificate-identity 'https://github.com/aquasecurity/private-trivy/.github/workflows/reusable-release.yaml@refs/tags/v0.69.2' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
--new-bundle-format \
ghcr.io/aquasecurity/trivy:0.69.2 \
| jq -er '.[0].critical.image."docker-manifest-digest"')
echo "verified: $TRIVY_DIGEST"
# after copying that digest into your own registry
podman run --rm "registry.example.com/mirror/trivy@${TRIVY_DIGEST}" image \
--db-repository registry.example.com/mirror/trivy-db:2 \
"registry.example.com/my-app@sha256:${DIGEST}"
The digests listed in the advisory are the compromised images. Check that yours isn't one of them, and never use them. If the exact identity fails for the image, read the identity from the image's own signing certificate and use that, but never loosen the match past ^https://github\.com/aquasecurity/private-trivy/.
When it makes sense to get help
Adding one scan step to one repository is a morning's work, and you should just do it. It gets harder when you have many repositories and teams, when findings pile up and nobody fixes them, or when an auditor wants to see that every image was scanned and signed before it ran. At that point it's a pipeline and platform problem more than a scanner problem, and that's the kind of work I do.
Related: the security gates I put in every pipeline and self-hosted runners on OpenShift.