HOWTOIntermediate

Container Security Scanning with Trivy: Images, IaC, and CI/CD

Learn how to use Trivy to scan container images, Dockerfiles, Kubernetes manifests, and Terraform for vulnerabilities and misconfigurations — then...

Dylan H.

Tutorials

March 27, 2026
7 min read
Container Security Scanning with Trivy: Images, IaC, and CI/CD

Introduction

Containers ship fast — and they carry their vulnerabilities with them. A single FROM ubuntu:20.04 layer can contain dozens of known CVEs, and misconfigured Kubernetes manifests are a leading cause of cloud breaches. Without automated scanning, these issues only surface after an incident.

Trivy (pronounced "try-vee") is an open-source, all-in-one security scanner from Aqua Security. A single tool covers:

  • OS package vulnerabilities — Alpine, Debian, Ubuntu, RHEL, Amazon Linux, and more
  • Language ecosystem dependencies — npm, pip, Maven, Go modules, Cargo, Composer, NuGet
  • Infrastructure as Code misconfigurations — Dockerfile, Kubernetes YAML, Terraform, CloudFormation, Helm
  • Exposed secrets — API keys, tokens, certificates, database connection strings (150+ pattern types)
  • License compliance — detect GPL/AGPL code in commercial projects

In this guide you'll go from a fresh Trivy install to a fully integrated scanning pipeline that catches issues before they reach production.


Prerequisites

Before starting, confirm you have:

  • Docker Engine 20.10+ (docker --version)
  • curl or a package manager for installation
  • 4 GB free disk space for the Trivy vulnerability database
  • A container image to practice with (we use public images throughout)

Step 1: Install Trivy

Option A — Install Script (Linux / macOS)

curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
  | sh -s -- -b /usr/local/bin v0.58.0

Verify:

trivy --version
# trivy version 0.58.0

Option B — Package Manager

Debian / Ubuntu:

sudo apt-get install -y wget apt-transport-https gnupg lsb-release
 
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key \
  | gpg --dearmor \
  | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
 
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] \
  https://aquasecurity.github.io/trivy-repo/deb \
  $(lsb_release -sc) main" \
  | sudo tee /etc/apt/sources.list.d/trivy.list
 
sudo apt-get update && sudo apt-get install -y trivy

RHEL / CentOS / Rocky Linux:

RELEASE=$(grep -Po '(?<=VERSION_ID=")[0-9]' /etc/os-release)
sudo tee /etc/yum.repos.d/trivy.repo << EOF
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$RELEASE/\$basearch/
gpgcheck=1
enabled=1
gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key
EOF
sudo yum -y install trivy

macOS (Homebrew):

brew install trivy

Option C — Docker (Zero Install)

Run Trivy as a container without installing anything locally:

docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $HOME/.cache/trivy:/root/.cache/trivy \
  aquasec/trivy:latest image nginx:latest

Mounting the Docker socket lets Trivy access locally built images. The cache volume prevents re-downloading the vulnerability database on every run.


Step 2: Your First Container Scan

Basic Image Scan

trivy image nginx:latest

On first run, Trivy downloads the vulnerability database (~200 MB). Subsequent runs use the local cache.

Output is grouped by layer/package and sorted by severity:

nginx:latest (debian 12.5)
==========================
Total: 142 (UNKNOWN: 0, LOW: 88, MEDIUM: 31, HIGH: 19, CRITICAL: 4)

┌──────────────────────┬────────────────┬──────────┬──────────────────────┬───────────────────┐
│       Library        │  Vulnerability │ Severity │  Installed Version   │   Fixed Version   │
├──────────────────────┼────────────────┼──────────┼──────────────────────┼───────────────────┤
│ libssl3              │ CVE-2024-5535  │ CRITICAL │ 3.0.11-1~deb12u2     │ 3.0.13-1~deb12u1  │
│ openssl              │ CVE-2024-5535  │ CRITICAL │ 3.0.11-1~deb12u2     │ 3.0.13-1~deb12u1  │
...

Filter to Critical and High Only

trivy image --severity CRITICAL,HIGH nginx:latest

This dramatically reduces noise, focusing your team on what matters most.

Compare Base Images

Alpine images carry far fewer OS vulnerabilities than Debian/Ubuntu:

# Debian-based — many findings
trivy image --severity CRITICAL,HIGH nginx:latest
 
# Alpine-based — minimal findings
trivy image --severity CRITICAL,HIGH nginx:1.26-alpine

Use this to make an informed base image choice early in your Dockerfile.

Scan a Locally Built Image

docker build -t myapp:dev .
trivy image --severity CRITICAL,HIGH myapp:dev

Scan a Private Registry Image

AWS ECR:

aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin \
    123456789012.dkr.ecr.us-east-1.amazonaws.com
 
trivy image 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest

Azure Container Registry:

az acr login --name myregistry
trivy image myregistry.azurecr.io/myapp:latest

Google Artifact Registry:

gcloud auth configure-docker us-central1-docker.pkg.dev
trivy image us-central1-docker.pkg.dev/myproject/myrepo/myapp:latest

Step 3: Output Formats

Trivy supports multiple output formats for integration with other tools.

# Default table output
trivy image nginx:latest
 
# JSON (for programmatic processing or dashboards)
trivy image --format json --output trivy-report.json nginx:latest
 
# SARIF (upload to GitHub Security tab / Azure DevOps)
trivy image --format sarif --output trivy-results.sarif nginx:latest
 
# CycloneDX SBOM (Software Bill of Materials)
trivy image --format cyclonedx --output sbom.json nginx:latest
 
# SPDX SBOM
trivy image --format spdx-json --output sbom-spdx.json nginx:latest
 
# HTML report
trivy image \
  --format template \
  --template "@contrib/html.tpl" \
  --output report.html \
  nginx:latest

The SARIF format is particularly useful — GitHub natively renders SARIF findings in the Security tab of any repository.


Step 4: Scan Infrastructure as Code

Dockerfile Scanning

Create a deliberately insecure Dockerfile to test with:

# insecure.Dockerfile
FROM ubuntu:18.04
USER root
COPY . /app
RUN apt-get update && apt-get install -y curl wget
EXPOSE 22
CMD ["/bin/bash"]

Scan it:

trivy config --file-patterns "Dockerfile:insecure.Dockerfile" .

Trivy will flag issues like running as root, exposing port 22, and using a deprecated base image.

Kubernetes Manifest Scanning

# Scan a single manifest
trivy config deployment.yaml
 
# Scan an entire manifests directory
trivy config ./k8s/
 
# Scan a Helm chart
trivy config ./charts/myapp/

Common findings in Kubernetes manifests:

  • AVD-KSV-0014 — Container running as root (runAsUser: 0)
  • AVD-KSV-0011 — CPU limits not set
  • AVD-KSV-0016 — Privileged container (privileged: true)
  • AVD-KSV-0030 — Default service account token mounted unnecessarily

Example secure pod security context:

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop: ["ALL"]

Terraform Scanning

trivy config ./terraform/

Trivy checks Terraform for misconfigurations like:

  • S3 buckets without server-side encryption or versioning
  • Security groups with 0.0.0.0/0 ingress on sensitive ports
  • RDS instances without encryption at rest
  • Lambda functions with overly permissive IAM roles
  • CloudTrail logging disabled

Step 5: Secret Detection

Trivy scans for accidentally committed secrets across filesystems and Git repositories.

# Scan a local directory
trivy fs --scanners secret .
 
# Scan a Git repo (clones and scans)
trivy repo https://github.com/myorg/myrepo
 
# Combine vuln, config, and secret scanning
trivy fs --scanners vuln,config,secret .

Trivy detects 150+ secret patterns including: AWS access keys, GitHub personal access tokens, Slack webhooks, JWT secrets, private RSA/EC keys, database connection strings, and SendGrid/Twilio/Stripe API keys.

Test Secret Detection

# Create a harmless test file (fake key format, safe to use in testing)
echo 'AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' > /tmp/test-secret.txt
trivy fs --scanners secret /tmp/test-secret.txt
rm /tmp/test-secret.txt

You should see a HIGH severity finding for the AWS secret.


Step 6: GitHub Actions Integration

Add Trivy to your CI/CD pipeline to block vulnerable images and misconfigured IaC before they merge.

Create .github/workflows/trivy-scan.yml:

name: Trivy Security Scan
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 6 * * 1"  # Weekly Monday 6am UTC
 
permissions:
  contents: read
  security-events: write  # Required to upload SARIF results
 
jobs:
  scan-image:
    name: Container Image Scan
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .
 
      - name: Cache Trivy DB
        uses: actions/cache@v4
        with:
          path: ~/.cache/trivy
          key: trivy-db-${{ github.run_id }}
          restore-keys: trivy-db-
 
      - name: Scan image with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: sarif
          output: trivy-image.sarif
          severity: CRITICAL,HIGH
          exit-code: "1"       # Fail the build on findings
          ignore-unfixed: true # Skip CVEs without a fix available
 
      - name: Upload results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()           # Upload even when the scan step fails
        with:
          sarif_file: trivy-image.sarif
          category: container-image
 
  scan-iac:
    name: IaC and Secret Scan
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout
        uses: actions/checkout@v4
 
      - name: Cache Trivy DB
        uses: actions/cache@v4
        with:
          path: ~/.cache/trivy
          key: trivy-db-${{ github.run_id }}
          restore-keys: trivy-db-
 
      - name: Scan IaC configs
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: config
          scan-ref: "."
          format: sarif
          output: trivy-iac.sarif
          exit-code: "1"
 
      - name: Scan for secrets
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: "."
          scanners: secret
          format: sarif
          output: trivy-secrets.sarif
          exit-code: "1"
 
      - name: Upload IaC results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-iac.sarif
          category: iac
 
      - name: Upload secret scan results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-secrets.sarif
          category: secrets

After the first run, findings appear under Security → Code scanning alerts in your GitHub repository.


Step 7: Advanced Configuration

.trivyignore — Suppress False Positives

Create a .trivyignore file at the repository root to suppress accepted risks:

# .trivyignore
# Format: <ID> [exp:<YYYY-MM-DD>] [ack:<note>]
 
# No fix available; mitigated by network segmentation policy
CVE-2023-44487
 
# Accepted false positive — only triggered in a path we don't execute
CVE-2024-21626 exp:2026-09-01 ack:reviewed by security team 2026-03-15
 
# IaC check — intentional, Terraform state bucket (no versioning needed)
AVD-AWS-0090
 
# Suppress a whole check type in a specific file
AVD-KSV-0020

Reference the file when scanning:

trivy image --ignorefile .trivyignore myapp:latest

trivy.yaml — Reusable Configuration

Avoid repeating flags by creating a trivy.yaml config file:

# trivy.yaml
scan:
  security-checks:
    - vuln
    - config
    - secret
 
severity:
  - CRITICAL
  - HIGH
 
format: table
exit-code: 0        # 0 for local dev; override to 1 in CI
 
image:
  ignore-unfixed: true
 
db:
  skip-update: false
 
cache:
  dir: ~/.cache/trivy

Use it with any scan command:

trivy image --config trivy.yaml myapp:latest
trivy config --config trivy.yaml ./terraform/

Air-Gapped / Offline Environments

In environments without internet access, pre-download the database and transfer it:

# On an internet-connected machine — download the DB bundle
trivy image --download-db-only
tar czf trivy-db.tar.gz ~/.cache/trivy/db/
 
# Transfer trivy-db.tar.gz to the air-gapped host, then:
mkdir -p ~/.cache/trivy/db/
tar xzf trivy-db.tar.gz -C ~/.cache/
 
# Scan without updating the DB
trivy image --skip-db-update --offline-scan myapp:latest

Verification

Run these checks to confirm your Trivy setup is working correctly.

1. Confirm Vulnerability Detection

Use a known-vulnerable image (safe to pull, just don't run it):

trivy image --severity CRITICAL python:3.6-slim 2>/dev/null | grep "CRITICAL"

You should see multiple CRITICAL findings — Python 3.6 is end-of-life with many unpatched CVEs.

2. Confirm IaC Scanning Works

cat > /tmp/test-pod.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: insecure-pod
spec:
  containers:
  - name: app
    image: nginx:latest
    securityContext:
      privileged: true
      runAsUser: 0
EOF
 
trivy config /tmp/test-pod.yaml
rm /tmp/test-pod.yaml

Expected: CRITICAL/HIGH findings for privileged: true and root user.

3. Confirm Secret Scanning Works

cat > /tmp/test-creds.env << 'EOF'
# Fake credentials — safe test data only
GITHUB_TOKEN=ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456
DATABASE_URL=postgresql://admin:s3cr3tpassword@db.internal/prod
EOF
 
trivy fs --scanners secret /tmp/test-creds.env
rm /tmp/test-creds.env

Expected: HIGH/CRITICAL findings for GitHub token and database credentials.

4. Verify GitHub Actions Results

After pushing your workflow:

gh run list --workflow=trivy-scan.yml --limit 5
gh run view <run-id> --log

Navigate to Security → Code scanning alerts in GitHub to confirm SARIF results uploaded successfully.


Troubleshooting

Database Download Fails

# Test connectivity to the Trivy DB registry
curl -I https://ghcr.io
 
# Force a fresh database download
trivy image --clear-cache nginx:latest
 
# Use the AWS mirror (useful behind proxies)
trivy image \
  --db-repository public.ecr.aws/aquasecurity/trivy-db \
  nginx:latest

"No Such Image" Error

# Pull the image first, then scan
docker pull myapp:latest
trivy image myapp:latest
 
# Or tell Trivy to pull from the registry directly
trivy image --image-src registry myapp:latest

Scan Is Slow or Times Out

# Reduce parallelism to avoid memory pressure
trivy image --parallel 1 myapp:latest
 
# Skip specific large directories (e.g., bundled JDK)
trivy image --skip-dirs /usr/local/java myapp:latest
 
# Only scan OS packages (skip language dependencies)
trivy image --vuln-type os myapp:latest

Too Many False Positives in CI

# Skip CVEs that have no available fix
trivy image --ignore-unfixed myapp:latest
 
# Add a project-wide .trivyignore for accepted risks
echo "CVE-2024-XXXXX exp:2026-12-31 ack:no-fix-upstream" >> .trivyignore
 
# Set a minimum score threshold
trivy image --severity CRITICAL myapp:latest

GitHub Actions SARIF Upload Rejected

The workflow job needs security-events: write permission. Confirm the permissions block is present:

permissions:
  contents: read
  security-events: write

If your organization restricts third-party Actions, pin the action to a specific SHA:

uses: aquasecurity/trivy-action@a20de5420d57c4102486cdd9349b532bf5b12c7  # v0.20.0

Summary

You've built a full container security scanning pipeline with Trivy:

TaskCommand
Scan a container imagetrivy image --severity CRITICAL,HIGH myapp:latest
Scan Dockerfile / K8s / Terraformtrivy config ./infra/
Detect committed secretstrivy fs --scanners secret .
Generate SARIF for GitHub--format sarif --output results.sarif
Scan a Git repositorytrivy repo https://github.com/org/repo
Suppress known false positives.trivyignore file
Offline / air-gapped scanning--skip-db-update --offline-scan

Trivy's breadth — one binary covering OS packages, app dependencies, IaC misconfigurations, and secrets — makes it the go-to choice for DevSecOps teams. Start with image scanning and severity filtering in CI, then progressively add IaC and secret detection as your pipeline matures.

Where to go next:

  • Run trivy k8s --report summary cluster to scan a live Kubernetes cluster
  • Deploy the Trivy Operator for continuous in-cluster scanning with automatic policy enforcement
  • Integrate Trivy with Harbor or AWS ECR to block vulnerable images at the registry level before they ever reach a deployment pipeline