Trivy is an open-source security scanner from Aqua Security that finds known vulnerabilities (CVEs) in container images, exposed secrets in files, and misconfigurations in Dockerfiles and Kubernetes manifests. In this tutorial you will install Trivy on Ubuntu 24.04 from its official APT repository, scan public and locally built images, filter the results to what you can act on, generate a Software Bill of Materials (SBOM), and make a CI pipeline fail when an image contains critical vulnerabilities.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has
sudoprivileges. - Docker Engine installed, with your user in the
dockergroup, to build and scan local images. - Outbound Internet access, because Trivy downloads its vulnerability database (a few hundred MB) on first use.
- At least 2 GB of free disk space for the database cache and the images you scan.
Step 1 - Installing Trivy from the official repository
Aqua Security publishes signed Debian packages. Install the tools needed to fetch the signing key:
sudo apt update
sudo apt install -y wget gnupg
Download the repository key into /etc/apt/keyrings:
sudo install -m 0755 -d /etc/apt/keyrings
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /etc/apt/keyrings/trivy.gpg
Add the repository, limited to that key with signed-by:
echo "deb [signed-by=/etc/apt/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list
Install the package:
sudo apt update
sudo apt install -y trivy
Confirm the installation:
trivy --version
Version: 0.74.0
The version number will be higher if a newer release is available. Updates arrive through apt upgrade like any other package.
Step 2 - Running your first image scan
Trivy can scan an image straight from a registry without pulling it into Docker first. Scan an older Python image, which is a good example because it contains many outdated packages:
trivy image python:3.10-slim-bullseye
The first run downloads the vulnerability database into ~/.cache/trivy, which takes a minute. Trivy then detects the operating system of the image, checks every OS package (from dpkg or apk) and every language package it finds (Python, Node.js, Go, Java and others) against the database, and prints a table per target.
The part to read first is the summary line for each target:
python:3.10-slim-bullseye (debian 11.11)
Total: 112 (UNKNOWN: 0, LOW: 71, MEDIUM: 25, HIGH: 14, CRITICAL: 2)
The exact numbers depend on the day you scan, because the database is updated several times a day. Each row of the table below it lists the package, the CVE ID, the severity, the installed version and, when a patch exists, the Fixed Version. That last column tells you whether updating the base image or the package would remove the finding.
Step 3 - Filtering results to what you can fix
A full report is too long to act on. Two filters make it practical:
--severitykeeps only the listed severities.--ignore-unfixedhides vulnerabilities that have no patch yet, since you cannot fix them by updating.
trivy image --severity HIGH,CRITICAL --ignore-unfixed python:3.10-slim-bullseye
The report now contains only the issues where an upgrade helps. Compare it with a current base image:
trivy image --severity HIGH,CRITICAL --ignore-unfixed python:3.13-slim
A current, slim base image usually reports far fewer findings. This is the cheapest improvement you can make: rebuild on a supported, recent base image and rebuild regularly so you pick up the distribution's security updates.
For scripts or dashboards, use JSON output and summarize it with jq:
sudo apt install -y jq
trivy image --quiet --format json --output report.json python:3.10-slim-bullseye
jq '[.Results[].Vulnerabilities[]?.Severity] | group_by(.) | map({(.[0]): length}) | add' report.json
{
"CRITICAL": 2,
"HIGH": 14,
"LOW": 71,
"MEDIUM": 25
}
Step 4 - Scanning an image you built locally
Trivy also scans images that exist only in your local Docker engine. Create a small test project:
mkdir -p ~/trivy-demo && cd ~/trivy-demo
nano Dockerfile
FROM python:3.10-slim-bullseye
WORKDIR /app
RUN pip install --no-cache-dir flask==2.0.1
COPY . .
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"]
This Dockerfile has three problems on purpose: an old base image, a pinned vulnerable Flask release and a container that runs as root. Build it:
docker build -t trivy-demo:latest .
Now scan it with the same flags you would use to gate a release. --exit-code 1 makes Trivy return a non-zero status when it finds issues at the selected severities:
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed trivy-demo:latest
echo "Exit code: $?"
The report now has two targets: the Debian packages of the base image and a Python section listing Flask with the version that fixes each CVE. The last line shows Exit code: 1, which is what makes a CI job fail.
If you exported an image with docker save, scan the tarball directly with --input:
docker save trivy-demo:latest -o trivy-demo.tar
trivy image --input trivy-demo.tar
Step 5 - Scanning project files for secrets and misconfigurations
Many problems are visible before you build an image. trivy fs scans a directory: lock files and requirement files for vulnerable dependencies, and all files for secrets such as API keys and private keys. trivy config checks Dockerfiles, Kubernetes manifests, Terraform and Helm charts for insecure settings.
Scan the demo project for vulnerable dependencies and leaked secrets:
trivy fs --scanners vuln,secret .
Check the Dockerfile for misconfigurations:
trivy config .
Trivy reports, among others, that the image runs as root because the Dockerfile has no USER instruction, and that no HEALTHCHECK is defined. Each finding includes an ID, a short explanation and a link to the rule's documentation.
You can scan a remote Git repository the same way without cloning it by hand:
trivy repo --scanners vuln,secret https://github.com/your_org/your_repo
Step 6 - Generating an SBOM
An SBOM is an inventory of every package inside an image. It is useful for audits, and it lets you re-check an image later against a newer database without having the image at hand. Generate one in CycloneDX format:
trivy image --format cyclonedx --output sbom.cdx.json trivy-demo:latest
Use --format spdx-json instead if your tooling expects SPDX. Check that the file lists the components:
jq '.components | length' sbom.cdx.json
Later, scan the SBOM itself for vulnerabilities:
trivy sbom sbom.cdx.json
Step 7 - Accepting known risks with .trivyignore
Some findings do not apply to your use case, for example a CVE in a library function your code never calls. Instead of lowering the severity threshold for everything, list those specific IDs in a .trivyignore file in the directory where you run Trivy:
nano .trivyignore
# Not exploitable here: the vulnerable feature is disabled (ticket SEC-123)
CVE-2023-30861 exp:2026-12-31
The exp: date makes the exception expire, so it is reviewed instead of being forgotten. Trivy reads .trivyignore from the current directory automatically; use --ignorefile path/to/file to point to another location. Keep this file in version control so exceptions are visible in code review.
Step 8 - Failing a CI pipeline on critical vulnerabilities
The same command from Step 4 works in any CI system. In GitHub Actions, the official action installs Trivy and runs the scan. Create .github/workflows/trivy.yml in your repository:
name: Container scan
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-24.04
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Build image
run: docker build -t my-app:${{ github.sha }} .
- name: Scan image with Trivy
uses: aquasecurity/[email protected]
with:
image-ref: my-app:${{ github.sha }}
format: table
exit-code: "1"
ignore-unfixed: true
severity: CRITICAL,HIGH
If the image contains a fixable HIGH or CRITICAL vulnerability, the step fails and the pull request shows a red check with the report in the job log.
TipThird-party actions run with access to your repository. For production pipelines, pin
aquasecurity/trivy-actionto a full commit SHA instead of a tag, and update it deliberately.
On other CI systems, install Trivy from the repository in Step 1 or run the official aquasec/trivy container image, then call trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed your_image.
Troubleshooting
The database download fails or hits a rate limit. Trivy downloads its database from a public registry. Retry later, or download it once with trivy image --download-db-only and reuse the cache. In CI, cache ~/.cache/trivy between runs.
Scanning a local image fails with permission denied on /var/run/docker.sock. Your user is not in the docker group. Run sudo usermod -aG docker $USER, log out and back in, and try again.
Air-gapped servers. On a connected machine, run trivy image --download-db-only, copy ~/.cache/trivy to the offline server, and scan there with --skip-db-update --offline-scan.
The cache grows too large. Remove cached data with trivy clean --all; the database is downloaded again on the next scan.
Conclusion
You installed Trivy from its official repository, scanned registry and local images, narrowed the results to fixable HIGH and CRITICAL issues, checked project files for secrets and Dockerfile mistakes, and produced an SBOM. With --exit-code 1 in your pipeline, images with known fixable vulnerabilities no longer reach production unnoticed. As next steps, harden your Dockerfiles with non-root users and minimal base images, scan Kubernetes manifests with trivy config, and schedule a weekly rescan of the images you already run, since new CVEs are published every day.
