Container Security Scanning with Trivy
Container images have become the de facto unit of deployment in modern cloud‑native environments, but they also introduce a new attack surface. An image that ships with outdated libraries or hidden secrets can compromise an entire cluster in seconds. That’s why integrating security scanning early and often is a non‑negotiable practice for any serious DevOps team.
In this article we’ll dive deep into Trivy – Aqua Security’s lightweight, open‑source scanner that can hunt down vulnerabilities, misconfigurations, and embedded secrets across Docker images, file systems, and Kubernetes manifests. By the end, you’ll not only be able to run Trivy locally, but also embed it into CI/CD pipelines, automate policy enforcement, and interpret its findings like a pro.
Why Trivy Stands Out
There are dozens of scanners on the market, yet Trivy’s popularity keeps climbing. Its key differentiators are:
- Zero‑configuration defaults – just install and run.
- Broad vulnerability database – merges CVE data from NVD, Red Hat, Alpine, and more.
- Misconfiguration detection – built‑in checks for Dockerfile best practices and Kubernetes security policies.
- Secret scanning – finds API keys, tokens, and passwords embedded in layers.
- Speed and low overhead – runs in seconds on typical images, making it CI‑friendly.
Because Trivy works on the client side, you retain full control over your data – no need to upload images to a SaaS platform unless you explicitly choose to.
Getting Started: Installation
Trivy is distributed as a single binary for Linux, macOS, and Windows. Choose the method that matches your environment.
Linux (apt)
# Add the official repository
sudo apt-get install -y gnupg curl
curl -fsSL https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list
# Install Trivy
sudo apt-get update
sudo apt-get install -y trivy
macOS (Homebrew)
brew install aquasecurity/trivy/trivy
Windows (Chocolatey)
choco install trivy
After installation, verify the version:
trivy --version
# Expected output: Trivy 0.xx.x (your version)
Pro tip: Run trivy image --download-db-only once a day via a cron job. This keeps the vulnerability database fresh without slowing down your builds.
Scanning a Docker Image
The most common use case is scanning a built image for known CVEs. Trivy automatically pulls the latest vulnerability database, inspects each layer, and reports findings.
# Scan a local image
trivy image myapp:latest
# Scan a remote image from Docker Hub
trivy image nginx:1.23-alpine
The output is a table with columns for Target, Vulnerability ID, Severity, and Description. By default, Trivy shows all severities; you can filter with --severity.
trivy image --severity HIGH,CRITICAL myapp:latest
For CI pipelines you typically want machine‑readable JSON:
trivy image -f json -o trivy-report.json myapp:latest
Understanding the Report
- FixedVersion: Indicates the version where the vulnerability is patched.
- PkgName: The affected package inside the image.
- Layer: Shows which image layer introduced the vulnerable component.
When a vulnerability has no FixedVersion, you may need to rebuild the base image or apply a manual patch.
Scanning Dockerfiles Directly
Often you want to catch misconfigurations before the image even exists. Trivy can analyze a Dockerfile and flag insecure instructions such as ADD with remote URLs, missing USER directives, or the use of --privileged in RUN commands.
trivy config --format table Dockerfile
The output lists the line number, the rule ID, severity, and a brief description.
Example: Detecting Hard‑Coded Secrets
Consider a Dockerfile that copies a configuration file containing an AWS secret key:
FROM python:3.10-slim
COPY config.yaml /app/config.yaml
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
If config.yaml contains AWS_SECRET_ACCESS_KEY=ABCD1234, Trivy will raise a secret finding:
trivy config --severity HIGH Dockerfile
Address the issue by externalizing the secret via environment variables or a secret manager.
Kubernetes Manifest Scanning
Containers don’t live in isolation; they run inside pods, services, and ingress resources. Misconfigurations at the Kubernetes level can expose your workloads to the internet, grant excessive privileges, or bypass network policies.
Trivy’s config subcommand can scan a directory of YAML manifests. It supports built‑in policies from the Open Policy Agent (OPA) and the Kubernetes Security Benchmark.
# Scan all manifests in the ./k8s directory
trivy config ./k8s
The report will highlight issues such as:
- Pods running as root.
- Containers with privileged mode enabled.
- Service accounts with cluster‑admin role bindings.
- NetworkPolicy missing for a namespace.
Custom Policy Example
Suppose your organization mandates that all containers must set a read‑only root filesystem. You can write a simple OPA rule and feed it to Trivy.
# file: policy.rego
package kubernetes.admission
deny[msg] {
container := input.spec.containers[_]
not container.securityContext.readOnlyRootFilesystem
msg = sprintf("Container %s does not have readOnlyRootFilesystem set", [container.name])
}
Run Trivy with the custom policy:
trivy config --policy policy.rego ./k8s
If any pod violates the rule, Trivy will output a DENY entry with the offending container name.
Pro tip: Store your OPA policies in a version‑controlled policies/ folder and reference them in your CI job. This creates a single source of truth for security standards across teams.
Integrating Trivy into CI/CD Pipelines
Automation is the key to consistent security. Below are two practical examples: one for GitHub Actions and another for GitLab CI.
GitHub Actions Workflow
name: Container Scan
on:
push:
branches: [ main ]
pull_request:
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build image
run: |
docker build -t myapp:${{ github.sha }} .
- name: Install Trivy
run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
- name: Scan image
id: scan
run: |
trivy image --format json -o trivy-report.json myapp:${{ github.sha }}
- name: Upload report
uses: actions/upload-artifact@v3
with:
name: trivy-report
path: trivy-report.json
- name: Fail on critical findings
run: |
jq '[.Results[].Vulnerabilities[] | select(.Severity=="CRITICAL")] | length' trivy-report.json | \
grep -q '^0$' || exit 1
This workflow builds the image, scans it, stores the JSON report as an artifact, and fails the job if any CRITICAL vulnerability is found.
GitLab CI Example
stages:
- build
- scan
build_image:
stage: build
script:
- docker build -t registry.example.com/myapp:$CI_COMMIT_SHA .
- docker push registry.example.com/myapp:$CI_COMMIT_SHA
trivy_scan:
stage: scan
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --format sarif --output trivy.sarif registry.example.com/myapp:$CI_COMMIT_SHA
artifacts:
reports:
sarif: trivy.sarif
expire_in: 1 week
allow_failure: false
GitLab’s native SARIF support surfaces findings directly in the Merge Request UI, making it easy for reviewers to see security issues alongside code changes.
Advanced Scanning Options
Trivy offers a rich set of flags that let you fine‑tune the scanning process. Below are the most useful for production environments.
--skip-db-update– Use the existing DB snapshot; handy in offline environments.--ignore-unfixed– Hide vulnerabilities that have no known fix yet.--scanners vuln,secret,config– Run only selected scanners to reduce runtime.--cache-dir /tmp/trivy-cache– Specify a custom cache location for CI runners.--severity– Filter results by severity (e.g.,MEDIUM,LOW).
Example: scanning a multi‑arch manifest while ignoring unfixed issues.
trivy image --platform linux/amd64,linux/arm64 \
--ignore-unfixed \
-f json -o multiarch-report.json \
myapp:latest
Interpreting Findings and Prioritizing Fixes
Not every vulnerability is equally urgent. Use the following pragmatic approach to triage:
- Identify the attack surface: Is the vulnerable package exposed via a network port? If it’s a dev‑only tool, the risk may be lower.
- Check for a fix: If
FixedVersionexists, upgrade the base image or the specific library. - Assess exploitability: CVSS scores and the presence of public exploits help gauge urgency.
- Apply mitigations: If an immediate upgrade isn’t possible, add runtime controls (e.g., AppArmor, seccomp) or network policies.
For misconfigurations, the remediation is often a single line change in a manifest. Treat these as “low‑effort, high‑impact” fixes and prioritize them early in the sprint.
Pro tip: Create a dashboard that aggregates Trivy JSON reports with a tool like Kibana or Grafana. Visualizing trends over time helps you demonstrate security improvements to stakeholders.
Real‑World Use Cases
1. Securing a CI‑Only Build Server
A fintech startup runs all builds on a hardened Jenkins node. By installing Trivy as a Jenkins plugin, every Docker push triggers an automated scan. The pipeline aborts on any HIGH or above vulnerability, ensuring that production never receives a risky image.
2. Enforcing Policy in a Multi‑Team Organization
In a large e‑commerce platform, each microservice team owns its own Helm chart. The platform team maintains a set of OPA policies (e.g., “no privileged containers”, “require resource limits”). Trivy runs nightly against the helm template output, and any violation opens a ticket in Jira automatically.
3. Detecting Secrets in Legacy Images
A legacy monolith container was built from a Dockerfile that copied a .env file containing database credentials. Running trivy image --scanners secret mylegacy:1.0 exposed the secret, prompting a migration to Kubernetes Secrets and a rebuild of the image without the embedded file.
Performance Considerations
While Trivy is fast, scanning large images or many manifests can still add minutes to your pipeline. Here are strategies to keep the impact minimal:
- Cache the DB: Store
/root/.cache/trivyon a persistent volume in your CI runners. - Layer‑based scanning: Use
--skip-filesand--skip-dirsto ignore non‑code directories like/usr/share/doc. - Parallel jobs: Run separate Trivy jobs for image scanning and config scanning to leverage multiple executors.
Benchmarking on a typical 500 MB Go microservice image shows:
| Operation | Time |
|---|---|
| DB update | ≈ 12 seconds |
| Full scan (vuln+config) | ≈ 8 seconds |
| Secret scan only | ≈ 3 seconds |
Handling False Positives
Occasionally Trivy will flag a package as vulnerable even though the specific version used in your image is not affected. Common causes include:
- Back‑ported patches that aren’t reflected in the upstream CVE database.
- Vulnerabilities in optional features that are not compiled into the binary.
When you encounter a false positive, verify the claim by checking the upstream project’s release notes. If confirmed, you can suppress the finding locally with an .trivyignore file:
# .trivyignore
CVE-2023-12345
Commit the ignore file alongside the Dockerfile so the suppression is version‑controlled.
Running Trivy in a Container
Sometimes you don’t want to install the binary on the host. Trivy provides an official Docker image that you can run as a sidecar.
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd):/project aquasec/trivy image myapp:latest