Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Trivy: vulnerability, secret and IaC scanner

By updated

Improve this page

Trivy

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.

TargetCommandTypical use
Folder or repositorytrivy fs .Dependencies, secrets, IaC of a project
Container imagetrivy image <image>OS and language packages in the image
Configuration filestrivy config .Dockerfile, Terraform, Kubernetes manifests
Existing SBOMtrivy sbom sbom.jsonRescan an inventory without the code
Whole systemtrivy rootfs /Packages installed on a machine
Clustertrivy k8sWorkloads 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.

Terminal window
TRIVY="aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969"
# The vulnerability database cache lives in a volume, reused from one scan to the next
docker 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.

Terminal window
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.

Terminal window
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:

Dockerfile
FROM python:3.12
RUN apt-get update && apt-get install -y curl
COPY . /app
CMD ["python", "/app/main.py"]
RuleSeverityWhat it flags
DS-0002HIGHNo non-root USER instruction
DS-0029HIGHapt-get install without --no-install-recommends
DS-0026LOWNo HEALTHCHECK instruction
AWS-0086, AWS-0087, AWS-0091, AWS-0093HIGHThe bucket's public access block lets public ACLs and policies through
AWS-0132HIGHNo encryption with a customer-managed key
AWS-0090MEDIUMVersioning disabled
AWS-0089LOWLogging disabled
infra/main.tf
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 imageVulnerabilitiesOf which CRITICALWith --ignore-unfixed
nginx:1.31.3-alpine (Alpine 3.24.1)54054
node:14 (Debian 10, end of life)1411211086
Terminal window
trivy image --scanners vuln \
nginx:1.31.3-alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752

The --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.

Terminal window
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.

trivy.yaml, added by a pull request
severity:
- CRITICAL

Measured 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.

The trivy.yaml trap: a pull request adds a trivy.yaml file, the CI job checks out the repository with this file, trivy fs loads it automatically (Loaded file_path=trivy.yaml) and the report is emptied without warning, the job stays green; the countermeasure, decided in the job, passes the options on the command line, enforces --config or deletes the file, and the report stays complete with its threshold.Pull requestadds trivy.yamlCI job: repository checked outwith the PR's trivy.yamltrivy fs .Loaded file_path=trivy.yamlReport emptied without warninggreen jobCountermeasure: command-line options,enforced --config or deleted fileComplete reportthreshold applied
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

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 --config or the TRIVY_CONFIG variable;
  • delete trivy.yaml, trivy.yml and .trivy.yaml before 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.

.trivyignore
# Component not exposed, fixed in release 2.0 (ticket SEC-142)
CVE-2021-23337 exp:2027-01-01

Generating 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.

Terminal window
# Produce the SBOM, without a vulnerability search
trivy fs --format cyclonedx --output sbom.json .
# Rescan it, today or in six months
trivy sbom sbom.json

Measured 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.

Terminal window
trivy image --compliance docker-cis-1.6.0 \
nginx:1.31.3-alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752

Measured 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.

.github/workflows/trivy.yml
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.sarif

The 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.

.gitlab-ci.yml
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.json

The 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.

Terminal window
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
kubectl get exposedsecretreports -A

What 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, misconfig is only active when requested.
  • After the 2026 incidents, you install it in a version pinned by digest, never latest nor v0.69.4.
  • It exits with code 0 by default: blocking is decided with --severity and --exit-code.
  • A trivy.yaml placed 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-unfixed reduces 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

6 questions
6 min
70% to pass

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

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.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn