Trivy: Container Image and IaC Security Scanning

Trivy: Container Image and IaC Security Scanning

Trivy is an open-source vulnerability scanner that covers container images (OS packages and application dependencies), Kubernetes manifests, Terraform, Dockerfiles, and secrets. It's fast, requires no setup beyond installation, and produces structured JSON or table output. Use Trivy in CI to block builds with critical vulnerabilities before they reach production.

Key Takeaways

Trivy scans container images without running them. It pulls the image layers, inventories all installed packages and application libraries, and checks them against vulnerability databases (CVE, OSV, GitHub Advisory Database).

Trivy covers more than just OS vulnerabilities. It also finds vulnerable npm, pip, gem, Go, and Maven packages—the application dependency layer that many image scanners miss.

Trivy finds secrets in images. It scans file contents in image layers for API keys, tokens, and other secrets accidentally included in container images.

Trivy scans Terraform and Kubernetes IaC. Misconfigurations in infrastructure code (overly permissive IAM, missing network policies, privileged containers) are found before deployment.

Use --exit-code 1 to fail CI on critical vulnerabilities. By default Trivy reports findings but exits 0. Add --exit-code 1 --severity CRITICAL,HIGH to block CI pipelines on high-severity findings.

Installation

# macOS
brew install trivy

<span class="hljs-comment"># Linux (Debian/Ubuntu)
<span class="hljs-built_in">sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key <span class="hljs-pipe">| <span class="hljs-built_in">sudo apt-key add -
<span class="hljs-built_in">echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main <span class="hljs-pipe">| <span class="hljs-built_in">sudo <span class="hljs-built_in">tee -a /etc/apt/sources.list.d/trivy.list
<span class="hljs-built_in">sudo apt-get update && <span class="hljs-built_in">sudo apt-get install trivy

<span class="hljs-comment"># Docker (no local install required)
docker run aquasec/trivy image nginx:latest

Scanning Container Images

Basic Scan

# Scan a public image
trivy image nginx:latest

<span class="hljs-comment"># Scan a specific image tag
trivy image python:3.11-slim

<span class="hljs-comment"># Scan a private image (authenticates using Docker credentials)
trivy image myregistry.example.com/myapp:1.2.3

Output shows:

  • Vulnerability ID (CVE number)
  • Package name and installed version
  • Fixed version (if a patch is available)
  • Severity (CRITICAL, HIGH, MEDIUM, LOW)
  • Description

Filter by Severity

# Only show CRITICAL and HIGH
trivy image --severity CRITICAL,HIGH nginx:latest

<span class="hljs-comment"># Fail CI if CRITICAL vulnerabilities found
trivy image --exit-code 1 --severity CRITICAL nginx:latest

Scan a Local Image

# Build first, then scan
docker build -t myapp:<span class="hljs-built_in">test .
trivy image myapp:<span class="hljs-built_in">test

<span class="hljs-comment"># Or scan from a tar file
docker save myapp:<span class="hljs-built_in">test -o myapp.tar
trivy image --input myapp.tar

Output Formats

# Default table output
trivy image nginx:latest

<span class="hljs-comment"># JSON output (for CI processing)
trivy image --format json --output trivy-results.json nginx:latest

<span class="hljs-comment"># SARIF output (for GitHub Code Scanning)
trivy image --format sarif --output trivy.sarif nginx:latest

<span class="hljs-comment"># Template-based HTML report
trivy image --format template --template <span class="hljs-string">"@contrib/html.tpl" --output report.html nginx:latest

Scanning Application Dependencies

Trivy scans application dependency files directly (without a container image):

# Scan current directory for vulnerable dependencies
trivy fs .

<span class="hljs-comment"># Scan specific manifest files
trivy fs package-lock.json
trivy fs requirements.txt
trivy fs go.sum
trivy fs Gemfile.lock
trivy fs pom.xml

Supported ecosystems:

  • npm (package-lock.json, yarn.lock)
  • pip (requirements.txt, Pipfile.lock, poetry.lock)
  • Go modules (go.sum, go.mod)
  • Ruby (Gemfile.lock)
  • Maven (pom.xml)
  • Gradle (gradle.lockfile)
  • Rust (Cargo.lock)
  • PHP (composer.lock)

Scanning Infrastructure as Code

Dockerfile Scanning

# Scan a Dockerfile for security misconfigurations
trivy config Dockerfile

Common findings:

  • Running as root user (USER root not followed by a non-root USER instruction)
  • Missing HEALTHCHECK instruction
  • ADD from URL (use COPY instead for better reproducibility)
  • Sensitive data exposed via ARG (visible in image history)
  • Latest tag used (non-deterministic builds)

Example Dockerfile with issues:

FROM ubuntu:latest           # ← Should pin a specific version
ARG API_KEY                  # ← ARG values appear in image history
ENV API_KEY=$API_KEY         # ← Secret in environment variable
RUN apt-get install curl     # ← Missing -y flag, and no apt-get update
COPY . /app
CMD ["python", "app.py"]     # ← Running as root

Terraform Scanning

# Scan Terraform directory
trivy config ./terraform/

<span class="hljs-comment"># Scan with Terraform plan output
terraform plan -out=tfplan
trivy config tfplan

Common Terraform findings:

  • S3 bucket with public ACL
  • Security groups with 0.0.0.0/0 ingress on port 22 or 3389
  • IAM policies with * actions or resources
  • CloudTrail logging disabled
  • EBS volumes not encrypted
  • RDS instances without encryption at rest

Kubernetes Manifest Scanning

# Scan Kubernetes YAML files
trivy config k8s/

<span class="hljs-comment"># Scan a specific manifest
trivy config deployment.yaml

Common Kubernetes findings:

  • Containers running as root (missing runAsNonRoot: true)
  • Privileged containers (privileged: true)
  • Containers with read-write root filesystem (missing readOnlyRootFilesystem: true)
  • Missing resource limits
  • Missing network policies
  • Containers with host path mounts

Secret Scanning in Images

Trivy can find secrets in container image layers—credentials accidentally committed and built into images:

trivy image --scanners secret nginx:latest

Enable secret scanning alongside vulnerability scanning:

trivy image --scanners vuln,secret myapp:latest

Common secret types detected:

  • AWS access keys and secret keys
  • GitHub tokens and PATs
  • Generic API keys and tokens
  • SSH private keys
  • Database connection strings with passwords
  • JWT secrets

Setting Vulnerability Ignore Policies

Some vulnerabilities have no fix (no patched version exists) or are not applicable to your use case. Use a .trivyignore file:

# .trivyignore
# CVE-2023-12345 - Not applicable: we don't use the vulnerable code path
CVE-2023-12345

# CVE-2023-67890 - No fix available, accepted risk per security team 2025-01-15
CVE-2023-67890

Or use an ignore policy file with expiry dates:

# trivy-ignore.yaml
ignoredVulnerabilities:
  - id: CVE-2023-12345
    statement: "Not applicable: we don't use Python's email module"
    expiredAt: "2025-12-31"
  - id: CVE-2023-67890
    statement: "No fix available, accepted risk approved by security team"
    expiredAt: "2025-06-01"
trivy image --ignorefile trivy-ignore.yaml myapp:latest

CI/CD Integration

GitHub Actions

# .github/workflows/trivy.yml
name: Container Security Scan
on:
  push:
    branches: [main]
  pull_request:

jobs:
  trivy-image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .
      
      - name: Run Trivy vulnerability scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          exit-code: '1'
          severity: 'CRITICAL,HIGH'
      
      - name: Upload to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif

  trivy-config:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run Trivy IaC scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: config
          scan-ref: .
          exit-code: '1'
          severity: 'CRITICAL,HIGH'

GitLab CI

# .gitlab-ci.yml
trivy-scan:
  stage: test
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity CRITICAL,HIGH $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  artifacts:
    when: always
    reports:
      container_scanning: trivy-results.json

Using Trivy in Docker Compose for Local Testing

# docker-compose.test.yml
services:
  trivy:
    image: aquasec/trivy:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./.trivyignore:/root/.trivyignore
    command: >
      image
      --severity CRITICAL,HIGH
      --exit-code 1
      myapp:test

Run as part of local testing:

docker compose -f docker-compose.test.yml run trivy

Practical Tips

Use distroless or minimal base images. alpine, distroless/python, distroless/java have far fewer vulnerabilities than ubuntu or debian because they have fewer installed packages. Fewer packages = smaller attack surface.

Pin base image digests in production. FROM nginx@sha256:abc123 guarantees reproducible builds and prevents supply chain attacks via tag mutation.

Scan early, scan often. Vulnerabilities in dependencies appear over time. A dependency that was safe when you added it may have a CVE six months later. Schedule daily or weekly scans of production images.

Fix by upgrading the base image first. Most OS-level vulnerabilities are fixed by updating to a newer base image version. FROM python:3.11.8-slimFROM python:3.11.9-slim fixes many CVEs automatically.

Use multi-stage builds to reduce the attack surface. Only include what's needed at runtime. Build dependencies (compilers, dev tools) shouldn't appear in the final image.

Comparing Base Images

Use Trivy to compare vulnerability counts across base images:

trivy image --severity CRITICAL,HIGH python:3.11-bullseye 2>&1 | grep <span class="hljs-string">"Total:"
trivy image --severity CRITICAL,HIGH python:3.11-slim-bullseye 2>&1 <span class="hljs-pipe">| grep <span class="hljs-string">"Total:"
trivy image --severity CRITICAL,HIGH python:3.11-alpine 2>&1 <span class="hljs-pipe">| grep <span class="hljs-string">"Total:"

This quickly shows which base image has the smallest vulnerability footprint.

Summary

Trivy is a comprehensive security scanner for the container and IaC ecosystem:

  • Container images: OS packages and application dependencies across all major ecosystems
  • Filesystems: scan dependency manifests without building an image
  • IaC: Terraform, Kubernetes, Dockerfile misconfigurations
  • Secrets: credentials accidentally included in image layers

Key CI integration points:

  1. Build your container image in CI
  2. Run trivy image --exit-code 1 --severity CRITICAL,HIGH to block on critical findings
  3. Upload SARIF to GitHub for review in pull requests
  4. Run trivy config . to catch IaC misconfigurations before deployment

Read more