Trivy Container Scanning: Scan Docker Images and Kubernetes Workloads in CI

Trivy Container Scanning: Scan Docker Images and Kubernetes Workloads in CI

Trivy is a fast, comprehensive open-source vulnerability scanner from Aqua Security that scans container images, filesystems, Git repositories, Kubernetes manifests, and IaC files. This guide covers local scanning, GitHub Actions integration, SBOM generation, and managing findings with .trivyignore.

Key Takeaways

Trivy scans more than just OS packages. It detects vulnerabilities in OS packages, language-specific packages (npm, pip, gem, cargo), misconfigurations in Kubernetes/Terraform/Dockerfile, and exposed secrets — all from a single binary. SBOM generation is increasingly required for compliance. Trivy can generate CycloneDX or SPDX SBOMs as a by-product of scanning, satisfying EO 14028 and emerging software supply chain regulations. Severity filtering prevents alert fatigue. Using --severity HIGH,CRITICAL limits CI failures to actionable findings while still producing a full report as a build artifact for security review. trivy config scans IaC files without running containers. Misconfigurations in Kubernetes manifests, Dockerfiles, and Terraform files are caught at the file level before deployment. The .trivyignore file provides version-controlled false positive management. Suppressed CVEs are tracked in source control with the same pull request workflow as code changes.

Trivy (from Aqua Security) has rapidly become the most popular open-source container vulnerability scanner. It is comprehensive — scanning OS packages and language ecosystems in a single pass — and fast, with a local database cache that makes subsequent scans take seconds rather than minutes. Its breadth of target types (images, filesystems, git repos, Kubernetes manifests, Terraform) means you can consolidate several security scanning tools into one.

This guide covers practical Trivy usage from local development through CI/CD pipelines to Kubernetes workload scanning.

Installing Trivy

# macOS
brew install aquasecurity/trivy/trivy

<span class="hljs-comment"># Ubuntu/Debian
<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">| gpg --dearmor <span class="hljs-pipe">| <span class="hljs-built_in">sudo <span class="hljs-built_in">tee /usr/share/keyrings/trivy.gpg > /dev/null
<span class="hljs-built_in">echo <span class="hljs-string">"deb [signed-by=/usr/share/keyrings/trivy.gpg] 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"># As a Docker container (no installation required)
docker run --<span class="hljs-built_in">rm aquasec/trivy:latest image nginx:1.25

Trivy downloads its vulnerability database on first run and caches it at ~/.cache/trivy. In CI, mount this directory as a cache volume to avoid downloading the database on every run.

Scanning Docker Images

Local Image Scanning

# Scan a local image (builds must happen first)
docker build -t myapp:latest .
trivy image myapp:latest

<span class="hljs-comment"># Scan a remote registry image
trivy image nginx:1.25

<span class="hljs-comment"># Filter by severity
trivy image --severity HIGH,CRITICAL nginx:1.25

<span class="hljs-comment"># Exit with code 1 if vulnerabilities found at threshold
trivy image --exit-code 1 --severity CRITICAL myapp:latest

<span class="hljs-comment"># Scan specific image layers (helps identify which Dockerfile instruction introduced a vulnerability)
trivy image --format json myapp:latest <span class="hljs-pipe">| jq <span class="hljs-string">'.Results[].Vulnerabilities[]? | {PkgName, VulnerabilityID, Severity, Layer: .Layer.DiffID}'

Understanding Trivy's Output

myapp:latest (debian 11.7)
=========================
Total: 3 (HIGH: 2, CRITICAL: 1)

┌─────────────────────┬────────────────┬──────────┬─────────────────────┬──────────────────┬─────────────────────────────────────────┐
│       Library       │ Vulnerability  │ Severity │   Installed Version │    Fixed Version │                  Title                  │
├─────────────────────┼────────────────┼──────────┼─────────────────────┼──────────────────┼─────────────────────────────────────────┤
│ libssl1.1           │ CVE-2023-0464  │ CRITICAL │ 1.1.1n-0+deb11u4   │ 1.1.1n-0+deb11u5 │ OpenSSL: Excessive Resource Usage       │
│ libpcre2-8-0        │ CVE-2022-41409 │ HIGH     │ 10.36-2            │ 10.36-2+deb11u1  │ PCRE2: denial of service                │
│ tar                 │ CVE-2023-39804 │ HIGH     │ 1.34+dfsg-1        │ 1.34+dfsg-1.1    │ tar: out-of-bounds read                 │
└─────────────────────┴────────────────┴──────────┴─────────────────────┴──────────────────┴─────────────────────────────────────────┘

Node.js (node-pkg)
==================
Total: 1 (HIGH: 1)

┌────────────────────┬────────────────┬──────────┬──────────────────────┬───────────────┬─────────────────────────────────────────┐
│       Library      │ Vulnerability  │ Severity │  Installed Version   │ Fixed Version │                  Title                  │
├────────────────────┼────────────────┼──────────┼──────────────────────┼───────────────┼─────────────────────────────────────────┤
│ semver             │ SNYK-JS-SEMVER │ HIGH     │ 7.3.8                │ 7.5.2         │ semver: Regular Expression Denial       │
└────────────────────┴────────────────┴──────────┴──────────────────────┴───────────────┴─────────────────────────────────────────┘

Trivy separates results by ecosystem — OS packages and Node.js packages each get their own section, making it clear where the vulnerability lives.

GitHub Actions Integration

Full Workflow with Caching

name: Container Security Scan

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  trivy-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Cache Trivy database
        uses: actions/cache@v4
        with:
          path: ~/.cache/trivy
          key: trivy-db-${{ github.run_id }}
          restore-keys: trivy-db-

      - name: Run Trivy vulnerability scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: "myapp:${{ github.sha }}"
          format: "sarif"
          output: "trivy-results.sarif"
          severity: "HIGH,CRITICAL"
          exit-code: "1"
          ignore-unfixed: true

      - name: Upload SARIF to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: "trivy-results.sarif"

      - name: Run Trivy scan (table output for logs)
        uses: aquasecurity/trivy-action@master
        if: always()
        with:
          image-ref: "myapp:${{ github.sha }}"
          format: "table"
          severity: "HIGH,CRITICAL"
          ignore-unfixed: true

The ignore-unfixed: true option is important — it suppresses CVEs for which no fix is available yet, since developers cannot act on them. This dramatically reduces noise without hiding genuinely fixable issues.

Scanning Kubernetes Manifests with trivy config

Trivy's config subcommand scans IaC files for misconfigurations without requiring running containers. It supports Kubernetes YAML, Helm charts, Dockerfiles, Terraform, and CloudFormation.

# Scan a directory of Kubernetes manifests
trivy config ./k8s/

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

<span class="hljs-comment"># Scan with specific checks enabled
trivy config --checks-bundle-url ghcr.io/aquasecurity/defsec:latest ./k8s/

<span class="hljs-comment"># Scan a Helm chart
trivy config --helm-values values.prod.yaml ./charts/myapp/

Example output for a Kubernetes deployment with security issues:

deployment.yaml (kubernetes)
============================
Tests: 28 (SUCCESSES: 22, FAILURES: 6, EXCEPTIONS: 0)
Failures: 6 (HIGH: 3, MEDIUM: 2, LOW: 1)

HIGH: Container 'app' of Deployment 'myapp' should set 'securityContext.allowPrivilegeEscalation' to false
════════════════════════════════════
Disabling privilege escalation prevents container processes from gaining additional privileges.
See https://avd.aquasec.com/misconfig/ksv020

HIGH: Container 'app' of Deployment 'myapp' should set 'securityContext.readOnlyRootFilesystem' to true
════════════════════════════════════
An immutable root filesystem helps prevent malicious binaries being added to PATH.

Adding trivy config to CI

  trivy-config-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Scan Kubernetes manifests
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: "config"
          scan-ref: "./k8s/"
          format: "sarif"
          output: "trivy-config.sarif"
          exit-code: "1"
          severity: "HIGH,CRITICAL"

      - name: Upload config scan SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: "trivy-config.sarif"

SBOM Generation

Software Bill of Materials (SBOM) generation has become a compliance requirement for many organisations following US Executive Order 14028. Trivy can generate SBOMs as a by-product of scanning.

# Generate CycloneDX SBOM
trivy image --format cyclonedx --output sbom.cdx.json myapp:latest

<span class="hljs-comment"># Generate SPDX SBOM
trivy image --format spdx-json --output sbom.spdx.json myapp:latest

<span class="hljs-comment"># Scan an existing SBOM for vulnerabilities
trivy sbom ./sbom.cdx.json

The two formats have different adoption profiles: CycloneDX is preferred in the DevSecOps toolchain (supported by Dependency-Track, OWASP), while SPDX is the ISO standard and preferred in government and automotive contexts.

Attaching SBOMs to Container Images

A modern practice is to sign and attach the SBOM to the container image using cosign and OCI image referrers:

# Generate and push SBOM as OCI attestation
trivy image --format cyclonedx --output sbom.json myapp:latest
cosign attest --predicate sbom.json --<span class="hljs-built_in">type cyclonedx myapp:latest

This creates a verifiable chain: the image is signed, and the SBOM is attached to that signature, allowing downstream consumers to verify both authenticity and component inventory.

Managing False Positives with .trivyignore

The .trivyignore file works similarly to .gitignore — list CVE IDs one per line and Trivy will skip them.

# .trivyignore
# CVE-2023-0464 - OpenSSL excessive resource usage
# Not exploitable in our deployment: we do not accept client certificates
# and the vulnerable code path requires a crafted certificate chain.
# Review date: 2026-09-01
CVE-2023-0464

# CVE-2022-41409 - PCRE2 denial of service
# Not exposed: regex operations only run on trusted internal data,
# never on user-supplied input.
# Review date: 2026-08-01
CVE-2022-41409

For more granular suppression — ignoring a CVE only for a specific package — use Trivy's VEX (Vulnerability Exploitability eXchange) document support:

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://example.com/vex/2026-001",
  "author": "security@example.com",
  "timestamp": "2026-05-19T00:00:00Z",
  "version": 1,
  "statements": [
    {
      "vulnerability": {"name": "CVE-2023-0464"},
      "products": [{"@id": "pkg:oci/myapp@sha256:abc123"}],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "impact_statement": "The vulnerable TLS certificate parsing code is not reachable in this deployment configuration."
    }
  ]
}

VEX documents are the emerging standard for machine-readable vulnerability disposition records, increasingly requested by enterprise customers and government procurement.

Scanning Filesystems and Git Repositories

Beyond container images, Trivy can scan local filesystems and Git repositories directly:

# Scan current directory (language packages, secrets, misconfigs)
trivy fs .

<span class="hljs-comment"># Scan a Git repository (without cloning)
trivy repo https://github.com/myorg/myapp

<span class="hljs-comment"># Scan for secrets only
trivy fs --scanners secret .

<span class="hljs-comment"># Scan for misconfigurations only
trivy fs --scanners misconfig ./infrastructure/

The --scanners flag lets you compose exactly what you want to check, avoiding noise from checks irrelevant to a particular scan target.

Output Formats Reference

Format Flag Use case
Table --format table Human-readable terminal output
JSON --format json Programmatic processing, dashboards
SARIF --format sarif GitHub Security, VS Code, SAST tools
CycloneDX --format cyclonedx SBOM generation
SPDX --format spdx-json SBOM (ISO standard)
Template --format template Custom HTML/CSV via Go templates

Summary

Trivy's combination of broad target support, fast scanning, and zero-configuration operation makes it well-suited for teams that want comprehensive container and IaC security coverage without maintaining multiple specialised tools. The .trivyignore file and emerging VEX document support provide version-controlled, auditable false positive management. Integrating both image scanning and trivy config IaC checks into pull request pipelines catches the two most common categories of container security issues — vulnerable dependencies in the image and misconfigured Kubernetes workload specs — before they reach production.

Read more