Container Security Scanning with Trivy
PROGRAMMING LANGUAGES Jan. 29, 2026, 11:30 a.m.

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:

  1. 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.
  2. Check for a fix: If FixedVersion exists, upgrade the base image or the specific library.
  3. Assess exploitability: CVSS scores and the presence of public exploits help gauge urgency.
  4. 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/trivy on a persistent volume in your CI runners.
  • Layer‑based scanning: Use --skip-files and --skip-dirs to 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:

OperationTime
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
Share this article