Scan Container Images with Trivy
Trivy is Aqua Security's open-source vulnerability scanner. It can scan container images, filesystems, IaC configs, and Kubernetes clusters for known CVEs and misconfigurations.
This guide shows how to run Trivy from an RWX run to scan container images. The pattern works in CI on every push, but it's especially useful as a scheduled job that scans your production images daily so a freshly disclosed CVE in something you already shipped surfaces quickly.
Install Trivy
Install Trivy in its own task so the install is cached and reused across runs:
- key: trivy-install
run: |
curl -fSLO https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.deb
sudo dpkg -i trivy_0.69.3_Linux-64bit.deb
rm trivy_0.69.3_Linux-64bit.deb
For arm64 base images, swap the .deb for trivy_0.69.3_Linux-ARM64.deb.
Cache the vulnerability database
Trivy downloads its vulnerability database (and Java DB, if applicable) on every scan, which is slow and can be rate-limited by GitHub Container Registry. Pull the DBs in a separate task with a TTL'd cache so they're refreshed periodically without being downloaded on every run:
- key: trivy
use: trivy-install
run: |
trivy image --download-db-only
trivy image --download-java-db-only
cache:
ttl: 12 hours
Tasks that depend on trivy will inherit the cached database via the merged filesystem.
Scan an image
To scan an image from a registry, pull it with Docker and run trivy image:
- key: scan-image
docker: true
use: trivy
run: |
docker pull "$IMAGE_TAG"
trivy image \
--scanners vuln \
--severity CRITICAL \
--exit-code 1 \
"$IMAGE_TAG"
env:
IMAGE_TAG: my-registry.example.com/my-app:latest
cache: false
A few things worth noting:
docker: truestarts a Docker daemon for the task sodocker pullworks.--scanners vulnlimits the scan to vulnerabilities. Trivy also scans for secrets and misconfigurations by default — drop the flag if you want those too.--severity CRITICALfilters down to critical-only findings. UseHIGH,CRITICALto widen the gate, or omit it to report everything.--exit-code 1makes Trivy fail the task when it finds a matching finding. Without it, the task succeeds even when vulnerabilities are reported.cache: falseensures the scan actually runs each time (necessary only if the image is mutable and can change between scans). With caching enabled, identical inputs (image tag, command) would skip the scan, which defeats the point of scanning against a fresh database.
Suppressing known issues
Use --ignorefile to suppress findings you've already triaged. Pair it with --show-suppressed to keep them visible in the output:
run: |
docker pull "$IMAGE_TAG"
trivy image \
--scanners vuln \
--severity CRITICAL \
--ignorefile ${{ run.dir }}/support/.trivyignore \
--show-suppressed \
--exit-code 1 \
"$IMAGE_TAG"
Commit support/.trivyignore alongside your run definition so the suppressions live with the code.
Scan production images on a schedule
The most common pattern is scanning a production image daily. Use a cron schedule to drive it, and pull the image from your registry — the example below uses ECR:
on:
cron:
- key: scan-image
schedule: '0 7 * * * America/New_York' # every day at 7am ET
target: scan-image
title: 'Daily image scan'
init:
aws-role: arn:aws:iam::123456789012:role/your-role
registry-host: 123456789012.dkr.ecr.us-east-2.amazonaws.com
remote-tag: 123456789012.dkr.ecr.us-east-2.amazonaws.com/your-app:latest
base:
image: ubuntu:24.04
config: rwx/base 1.0.3
tasks:
- key: aws-cli
call: aws/install-cli 1.0.11
- key: aws
use: aws-cli
call: aws/assume-role 2.0.9
with:
region: us-east-2
role-to-assume: ${{ init.aws-role }}
role-duration-seconds: 900
- key: ecr
use: aws
run: |
cat <<EOF > $RWX_HOOKS_BEFORE_TASK/ecr.sh
aws ecr get-login-password --region us-east-2 \
| docker login --username AWS --password-stdin $REGISTRY_HOST
EOF
chmod +x $RWX_HOOKS_BEFORE_TASK/ecr.sh
env:
AWS_SKIP_AUTH: true
REGISTRY_HOST: ${{ init.registry-host }}
- key: trivy-install
run: |
curl -fSLO https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.deb
sudo dpkg -i trivy_0.69.3_Linux-64bit.deb
rm trivy_0.69.3_Linux-64bit.deb
- key: trivy
use: trivy-install
run: |
trivy image --download-db-only
trivy image --download-java-db-only
cache:
ttl: 12 hours
- key: scan-image
docker: true
use: [trivy, ecr]
run: |
docker pull "$IMAGE_TAG"
trivy image \
--scanners vuln \
--severity CRITICAL \
--exit-code 1 \
--show-suppressed \
"$IMAGE_TAG"
env:
AWS_OIDC_TOKEN:
value: ${{ vaults.default.oidc.aws }}
cache-key: excluded
IMAGE_TAG: ${{ init.remote-tag }}
cache: false
outputs:
filesystem: false
A few things worth noting:
- Writing the ECR
docker logininto$RWX_HOOKS_BEFORE_TASKon theecrtask means downstream tasks re-authenticate at the start of each task they run in, which keeps short-lived AWS credentials from expiring mid-run. AWS_OIDC_TOKENis markedcache-key: excludedso the rotating token doesn't bust the task cache.outputs.filesystem: falseskips uploading the task filesystem — there's nothing downstream consumes it.- A cron-driven failure surfaces in RWX as a failed run on the schedule. Wire up notifications so a fresh CVE actually pages someone.