
Trivy is the open source security scanner from Aqua Security, released in 2019 and followed by more than 38,000 stars on GitHub. In a single command, it looks for known vulnerabilities in dependencies and images, secrets forgotten in the code and misconfigurations in infrastructure as code. This lesson is for a DevOps engineer who wants a single tool for these three checks, and wants it to block in CI.
In 2026, Trivy also became a textbook case of what it is meant to detect:
in March, stolen credentials made it possible to publish a booby-trapped
version and to move 76 of the 77 tags of its GitHub action to a secret
stealer. So you install it like any other dependency, pinned. Everything
else is measured on Trivy 0.74.0: 4 vulnerabilities, a GitHub token and 10
misconfigurations on a test project, 1411 vulnerabilities on the node:14
image, and a two-line trivy.yaml that empties the report without a word.
What you will learn
- Install Trivy in a pinned version, after its two incidents of 2026
- Scan dependencies, secrets, images and infrastructure as code
- Make a CI job fail on a chosen threshold, not on the default
- Neutralise the trap of the automatically loaded
trivy.yaml - Produce an SBOM and rescan it without the source code
- Place what Trivy does not see, and when to cross-check with Grype
What is Trivy?
Trivy combines a target, what you scan, and scanners, what you look for in it. The same command thus serves to check a repository before the merge, an image before publication or a Terraform folder before deployment. It relies on a vulnerability database that it downloads and caches, and on built-in rules for misconfigurations.
| Target | Command | Typical use |
|---|---|---|
| Folder or repository | trivy fs . | Dependencies, secrets, IaC of a project |
| Container image | trivy image <image> | OS and language packages in the image |
| Configuration files | trivy config . | Dockerfile, Terraform, Kubernetes manifests |
| Existing SBOM | trivy sbom sbom.json | Rescan an inventory without the code |
| Whole system | trivy rootfs / | Packages installed on a machine |
| Cluster | trivy k8s | Workloads and configuration of a cluster |
The scanners are enabled with --scanners: vuln (known
vulnerabilities), secret (tokens and passwords), misconfig
(misconfigurations) and license (licences). Measured: on a folder, trivy fs
runs vuln and secret without being asked, and only analyses configurations
when misconfig is explicitly requested.
Installing a pinned version
After the 2026 incidents, the rule is simple: you do not install "the latest", you install a precise version, at least 0.72.0, verified. The official image pinned by digest is the safest form, in CI as on the workstation.
TRIVY="aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969"
# The vulnerability database cache lives in a volume, reused from one scan to the nextdocker run --rm -v "$PWD:/src" -w /src -v trivy-cache:/root/.cache "$TRIVY" fs .On the workstation, a version manager such as
asdf (in French) pins the version in the
repository: asdf install trivy 0.74.0 then asdf set trivy 0.74.0, which
writes it into .tool-versions. Avoid latest, which would silently
install the next release, sound or not.
Scanning a project: dependencies and secrets
The test bench for this lesson is a small deliberately faulty project: a
requirements.txt that pins requests==2.25.1, a config.py file that
contains a fake GitHub token, a Dockerfile and a Terraform file that are badly
configured. Run without options, Trivy finds the first two problems.
trivy fs .requirements.txt (pip)Total: 4 (UNKNOWN: 0, LOW: 0, MEDIUM: 4, HIGH: 0, CRITICAL: 0)
config.py (secrets)Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 1)CRITICAL: GitHub (github-pat)GitHub Personal Access Token 1 [ GITHUB_TOKEN = "****************************************"Two points can be read in this output. The secret is classified CRITICAL
and its value is masked in the report, which avoids copying it into a CI
log. And neither the Dockerfile nor the Terraform file appears: without
--scanners misconfig, Trivy does not analyse them. A "0 problems" on an
infrastructure folder can therefore simply mean that the scanner did not
look.
Analysing infrastructure as code
The misconfig scanner compares configuration files with built-in rules: a container running as root, a bucket open to the public, missing encryption. It covers Dockerfile, Terraform, Kubernetes, Helm and CloudFormation.
trivy fs --scanners vuln,secret,misconfig .# or, for configurations only:trivy config .Measured on the two files of the test bench: 3 findings for the Dockerfile and 7 for the Terraform file, each with its rule identifier:
FROM python:3.12RUN apt-get update && apt-get install -y curlCOPY . /appCMD ["python", "/app/main.py"]| Rule | Severity | What it flags |
|---|---|---|
DS-0002 | HIGH | No non-root USER instruction |
DS-0029 | HIGH | apt-get install without --no-install-recommends |
DS-0026 | LOW | No HEALTHCHECK instruction |
AWS-0086, AWS-0087, AWS-0091, AWS-0093 | HIGH | The bucket's public access block lets public ACLs and policies through |
AWS-0132 | HIGH | No encryption with a customer-managed key |
AWS-0090 | MEDIUM | Versioning disabled |
AWS-0089 | LOW | Logging disabled |
resource "aws_s3_bucket" "donnees" { bucket = "mon-bucket-de-test"}
resource "aws_s3_bucket_public_access_block" "donnees" { bucket = aws_s3_bucket.donnees.id block_public_acls = false block_public_policy = false}A rule is not an absolute truth: a static website bucket is public by design. The right answer is a documented exception, not disabling the scanner. The next lesson, on Checkov, goes deeper into IaC analysis with a second tool.
Scanning a container image
trivy image inventories the operating system packages of the image and
those of the languages it embeds, then compares them with its database. The
result depends above all on the age of the base image. Measured on
1 October 2026:
| Scanned image | Vulnerabilities | Of which CRITICAL | With --ignore-unfixed |
|---|---|---|---|
nginx:1.31.3-alpine (Alpine 3.24.1) | 54 | 0 | 54 |
node:14 (Debian 10, end of life) | 1411 | 21 | 1086 |
trivy image --scanners vuln \ nginx:1.31.3-alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752The --ignore-unfixed option hides vulnerabilities without a published
fix. It changes nothing on the recent Alpine image, where everything
already has a fix, and removes 325 findings on node:14. It reduces the
noise of what the team cannot fix, but must not make you forget that an
end-of-life image will no longer receive fixes: the only way out is to
change the base image. The figures move every day with the vulnerability
database; read them as an order of magnitude.
Making the pipeline fail
Measured: Trivy exits with code 0 whatever its findings. Without options, it informs and lets things through. Blocking is decided with two options: the severity retained and the desired exit code.
trivy image --scanners vuln --severity CRITICAL --exit-code 1 "$IMAGE"On the bench's requirements.txt, which only contains 4 MEDIUM
vulnerabilities, --severity HIGH,CRITICAL --exit-code 1 exits with 0,
which is intended, and --severity MEDIUM --exit-code 1 exits with 1.
Always test the threshold in both directions. The choice of the
severities that block, and the place of this check among the others, are the
subject of the lesson on the
secure pipeline.
The trap of the automatically loaded trivy.yaml
Trivy loads on its own a trivy.yaml file present in the current folder.
That is convenient on a workstation, and dangerous in CI: a pull
request that adds this file changes the behaviour of the scanner that is
supposed to judge it.
severity: - CRITICALMeasured on the bench: the log shows Loaded file_path="trivy.yaml", and the
4 MEDIUM vulnerabilities disappear from the report, without warning.
By adding scan: { scanners: [vuln] }, it is the CRITICAL GitHub token
that disappears in turn. The diagram follows this path, from the pull
request to the green job, and shows the countermeasure, decided in
the job itself and not in the repository that the PR can modify.
Three countermeasures, to be combined:
- pass the options on the command line (
--severity,--scanners,--exit-code), which take precedence over the file; - enforce a controlled configuration file, outside the PR's code, with
--configor theTRIVY_CONFIGvariable; - delete
trivy.yaml,trivy.ymland.trivy.yamlbefore the scan when the repository is checked out, or scan the image without checking out the repository.
Documenting exceptions
An accepted finding is declared in a .trivyignore file, with an
expiry date: after that date, the exception lapses and the finding comes
back. The lesson on SCA
measured it, as well as the trap of the .trivyignore.yaml variant, which is
only read with --ignorefile.
# Component not exposed, fixed in release 2.0 (ticket SEC-142)CVE-2021-23337 exp:2027-01-01Generating and rescanning an SBOM
An SBOM (Software Bill of Materials) is the inventory of the components of a piece of software. Trivy produces it in CycloneDX or SPDX format, then can rescan this inventory later, without the code or the image: that is what lets you find out within minutes whether a new CVE affects a version already shipped.
# Produce the SBOM, without a vulnerability searchtrivy fs --format cyclonedx --output sbom.json .
# Rescan it, today or in six monthstrivy sbom sbom.jsonMeasured on the bench: the SBOM is in CycloneDX 1.7 format and contains 2
components, requests and its declaration; trivy sbom finds in it the 4
MEDIUM vulnerabilities of the direct scan. Producing an SBOM runs no
vulnerability search: these are two distinct steps. The supply chain module
devotes a lesson to the SBOM.
Checking an image's CIS compliance
Trivy can evaluate an image against the CIS Docker Benchmark, for the part that concerns images (section 4). Manual checks, such as "use trusted base images", remain without a verdict.
trivy image --compliance docker-cis-1.6.0 \ nginx:1.31.3-alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752Measured on nginx:1.31.3-alpine: 4.1 (create a user for the container)
and 4.6 (add a HEALTHCHECK) are in FAIL; 4.4, 4.7, 4.9 and 4.10,
including "no secrets in Dockerfiles", pass. The first failure matches what
the lesson on Docker security
showed: the official nginx image starts as root.
Integrating Trivy into CI
In CI, the simplest and safest approach is to run the official image pinned
by digest, as on the workstation: it was the aquasecurity/trivy-action
action that was compromised in March 2026, and calling the image directly
removes that intermediary. The workflow below builds the image, scans it with
an explicit threshold and publishes the SARIF report in the
Security tab.
name: Trivy
on: pull_request: push: branches: [main]
permissions: {}
jobs: scan: runs-on: ubuntu-24.04 permissions: contents: read security-events: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Build the image run: docker build -t "mon-app:$GITHUB_SHA" .
- name: Neutralise a Trivy configuration coming from the PR run: rm -f trivy.yaml trivy.yml .trivy.yaml
- name: Scan, blocking on CRITICAL env: TRIVY: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 run: | docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$PWD:/out" "$TRIVY" \ image --scanners vuln --format sarif --output /out/trivy.sarif "mon-app:$GITHUB_SHA" docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$TRIVY" \ image --scanners vuln --severity CRITICAL --exit-code 1 "mon-app:$GITHUB_SHA"
- name: Publish the report if: always() uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: sarif_file: trivy.sarifThe Docker socket is mounted here only in the Trivy container, for the
duration of the scan, so that it can read the image built on the runner;
no application container has access to it. On GitLab, the same logic is
written with the pinned image as the job's image: and GIT_STRATEGY: none
when scanning an image from the registry: with no repository checked out, no
trivy.yaml can sneak in.
container_scanning: image: name: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 entrypoint: [""] variables: GIT_STRATEGY: none TRIVY_USERNAME: "$CI_REGISTRY_USER" TRIVY_PASSWORD: "$CI_REGISTRY_PASSWORD" TRIVY_AUTH_URL: "$CI_REGISTRY" TRIVY_CACHE_DIR: ".trivycache/" FULL_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA script: - trivy image --exit-code 0 --format template --template "@/contrib/gitlab.tpl" --output "$CI_PROJECT_DIR/gl-container-scanning-report.json" "$FULL_IMAGE_NAME" - trivy image --exit-code 1 --severity CRITICAL "$FULL_IMAGE_NAME" cache: paths: - .trivycache/ artifacts: when: always reports: container_scanning: gl-container-scanning-report.jsonThe first command produces the report displayed in the merge request,
without blocking; the second carries the threshold. The vulnerability
database cache (TRIVY_CACHE_DIR) avoids downloading it again in every
job.
Monitoring a cluster with Trivy Operator
For continuous scanning of the workloads of a Kubernetes cluster, Trivy
Operator runs Trivy inside the cluster and stores its results in Kubernetes
resources. The project is maintained: chart 0.36.0 (operator 0.34.0) is
dated 24 August 2026, after three versions in two months. Its values.yaml
runs Trivy 0.74.0, but by tag only: the chart provides no digest
field, and the aquasec/trivy-checks rule bundle is referenced there by the
floating tag 1. On a sensitive cluster, pin these references yourself in the
values. The operator was not deployed in this lesson.
helm repo add aqua https://aquasecurity.github.io/helm-charts/helm install trivy-operator aqua/trivy-operator \ --namespace trivy-system --create-namespace --version 0.36.0
kubectl get vulnerabilityreports -Akubectl get configauditreports -Akubectl get exposedsecretreports -AWhat Trivy does not see
Trivy compares what it finds with databases and rules: it only sees what is in them. A vulnerability not yet published (0-day), a flaw specific to your code, a binary copied by hand without metadata, or a business logic error escape it. It complements SAST and DAST, it does not replace them.
Two scanners do not see the same thing either. Measured in the lesson on
CVEs: on the same node:14
image, Grype found 2032 matches where Trivy reported 1455, because they
neither read the same databases nor read the components in the same way.
Cross-checking two tools reduces the blind spots without removing them.
Key points
- Trivy scans dependencies, secrets, images and IaC; on a folder,
misconfigis only active when requested. - After the 2026 incidents, you install it in a version pinned by digest, never
latestnor v0.69.4. - It exits with code 0 by default: blocking is decided with
--severityand--exit-code. - A
trivy.yamlplaced in the repository is loaded automatically and can empty the report: command-line options, enforced configuration, deleted file. - A CycloneDX SBOM can be rescanned later with
trivy sbom, without the code. --ignore-unfixedreduces the noise, but an end-of-life image gets replaced.
Knowledge check
Check that you have the essentials of this lesson. The quiz only covers what has just been explained here, from Trivy scanners to the CI threshold.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Promoting an artifact: build once, deploy twice
A new full quiz with random questions
Work again only on the questions you missed
FAQ: frequently asked questions
The questions below cover the most common doubts about Trivy: installation, targets, CI integration and limits, with the same pinned versions as the lesson.
| Target | Command | What it looks for |
|---|---|---|
| Folder or repository | trivy fs . |
Dependencies, secrets, IaC |
| Image | trivy image <image> |
OS and language packages |
| Configuration | trivy config . |
Dockerfile, Terraform, Kubernetes |
| SBOM | trivy sbom sbom.json |
Components of an existing inventory |
node:14 image: Grype found 2032 matches, Trivy 1455.| Criterion | Trivy | Grype |
|---|---|---|
| Vendor | Aqua Security | Anchore |
| Targets | Images, folders, IaC, SBOM, Kubernetes | Images, folders, SBOM |
| Secrets and IaC | Yes | No |
| EPSS and KEV in the report | No | Yes |
latest. The safest form is the official image pinned by digest:TRIVY="aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969"
docker run --rm -v "$PWD:/src" -w /src -v trivy-cache:/root/.cache "$TRIVY" fs .
On the workstation, a version manager pins the version in the repository:asdf install trivy 0.74.0
asdf set trivy 0.74.0
To ban: 0.69.4, Docker Hub images 0.69.5 and 0.69.6 (advisory GHSA-69fq-xp46-6x23), and releases 0.27.0 to 0.69.1, deleted during the February incident.trivy image --scanners vuln nginx:1.31.3-alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752
# Only the vulnerabilities that have a fix
trivy image --scanners vuln --ignore-unfixed <image>
# Block on CRITICAL
trivy image --scanners vuln --severity CRITICAL --exit-code 1 <image>
Measured on 1 October 2026: nginx:1.31.3-alpine carries 54 vulnerabilities, all fixable; node:14, end of life, carries 1411, of which 1086 have a fix. Always designate the image by its digest, not by latest.trivy fs reads the dependency files (requirements.txt, package-lock.json, go.mod, pom.xml...) and compares them with its database.trivy fs .
Measured: without options, trivy fs runs the vuln and secret scanners. On a requirements.txt that pins requests==2.25.1, it reports 4 MEDIUM vulnerabilities. The configuration scan (misconfig) is only active when requested with --scanners vuln,secret,misconfig.trivy config .
# or, with the rest:
trivy fs --scanners vuln,secret,misconfig .
Measured: a Dockerfile without USER raises DS-0002 (HIGH), an apt-get install without --no-install-recommends raises DS-0029, and an S3 bucket with its public access block disabled raises AWS-0086, AWS-0087, AWS-0091 and AWS-0093. A rule that does not apply to your case is documented as an exception, it is not disabled.trivy fs --format cyclonedx --output sbom.json .
trivy sbom sbom.json
Measured: the SBOM comes out in CycloneDX 1.7 format, and trivy sbom finds in it the same vulnerabilities as the direct scan. Producing the SBOM runs no vulnerability search: these are two distinct steps. See the SBOM guide (in French).aquasecurity/trivy-action that was compromised in March 2026.permissions: {}
jobs:
scan:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: rm -f trivy.yaml trivy.yml .trivy.yaml
- run: >
docker run --rm -v "$PWD:/src" -w /src
aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
fs --scanners vuln,secret --severity CRITICAL --exit-code 1 .
The rm neutralises a Trivy configuration brought by the pull request. On GitLab, use the same image as the job's image:. Complete examples in the Trivy guide..trivyignore file declares the accepted findings, ideally with an expiry date: after that date, the exception lapses and the finding comes back.# Component not exposed, fixed in release 2.0 (ticket SEC-142)
CVE-2021-23337 exp:2027-01-01
Measured in the lesson on SCA: .trivyignore is read by default, whereas the .trivyignore.yaml variant is only read with --ignorefile. Each line carries its justification. See SCA.helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm install trivy-operator aqua/trivy-operator \
--namespace trivy-system --create-namespace --version 0.36.0
kubectl get vulnerabilityreports -A
kubectl get configauditreports -A
Chart 0.36.0 (operator 0.34.0, 24 August 2026) runs Trivy 0.74.0, but by tag only, and references the rule bundle by the floating tag 1: pin these references yourself on a sensitive cluster.- cache the vulnerability database between jobs (
TRIVY_CACHE_DIR, or a volume locally) so as not to download it again; - limit the scanners to what the job checks, for example
--scanners vulnfor an image; - limit the output to the useful threshold with
--severity.
trivy image --download-db-only
trivy image --skip-db-update --scanners vuln <image>
A cache that is too old misses recent vulnerabilities: refresh the database at least once a day.| Format | Use | Option |
|---|---|---|
| table | Human reading, default | --format table |
| json | Automated processing | --format json |
| sarif | GitHub Security tab | --format sarif |
| cyclonedx | CycloneDX SBOM | --format cyclonedx |
| spdx-json | SPDX SBOM | --format spdx-json |
| template | GitLab report | --format template --template "@/contrib/gitlab.tpl" |
trivy sbom.trivy fs and trivy image.trivy fs .
Measured: a fake ghp_ token in a Python file is detected as GitHub Personal Access Token, classified CRITICAL, and its value is masked in the report. To check for secrets across the whole Git history, a dedicated tool such as Gitleaks remains more complete.| Limit | Consequence | Complement |
|---|---|---|
| Unpublished vulnerabilities (0-day) | Invisible | Monitoring, defence in depth |
| Flaws specific to your code | Invisible | SAST |
| Runtime behaviour | Invisible | DAST |
| Configuration in the repository | A trivy.yaml can empty the report |
Command-line options |
trivy.yaml placed in the folder is loaded automatically and makes the findings disappear from the report, without warning. See the Trivy guide.trivy rootfs scans the packages installed on a file system, local or mounted; trivy vm targets a virtual machine image.# The local system, with the rights needed to read all the packages
sudo trivy rootfs --scanners vuln --severity HIGH,CRITICAL /
# A VM image
trivy vm --severity HIGH,CRITICAL ./disk.qcow2
These two commands were not measured in the lesson. For auditing a server's configuration, beyond packages, a tool such as Lynis (in French) is better suited.