Sheet ⁨06⁩ · ⁨DevTips⁩Surveyed ⁨2026⁩

Blog post image for Container Image Vulnerability Scanning in CI/CD with Trivy - How to automate container image vulnerability scanning in CI/CD with Trivy: installation, severity thresholds, GitHub Actions and GitLab CI integration, policy enforcement, and remediation workflows.

Container Image Vulnerability Scanning in CI/CD with Trivy

Published: 04 Mins read04 Mins listen
Markdown for AI(opens in a new tab)

Why container security matters

Where the vulnerabilities hide

A container image is one of the largest pieces of untrusted code you ship.

Every image you build carries the base OS layer, runtime libraries, your dependencies and your application code. Any of those layers can hold known CVEs, and most of them you didn’t write. Without scanning, that whole stack reaches production unread.

The numbers

  • 80% of container images in production contain at least one known vulnerability
  • Supply chain attacks targeting container registries are increasing
  • Unpatched container vulnerabilities lead to data breaches and service disruptions

The challenge

Why manual review isn’t enough

Nobody is going to read the dependency tree of every image on every build. Without automation, a vulnerable image ships, and you find out about it from a CVE feed or an incident rather than from the pipeline that built it.

Common vulnerabilities in containers

  • Outdated base images with unpatched OS vulnerabilities
  • Vulnerable dependencies pulled in from npm, pip or Maven
  • Exposed secrets accidentally included in image layers
  • Misconfigurations creating insecure defaults
  • Malware hidden in supply chain attacks

The fix: Trivy

What Trivy is

Trivy is a fast container vulnerability scanner from Aqua Security. It scans container images, filesystems and configuration files for known vulnerabilities, misconfigurations and secrets.

Why I reach for Trivy

  • Speed: Scans images in seconds, not minutes
  • Accuracy: Supports multiple vulnerability databases (NVD, GitHub Security, Aqua, Alpine)
  • Broad coverage: Detects OS vulnerabilities, application dependencies, and misconfigurations
  • Zero setup: Works out of the box without complex configuration
  • CI/CD ready: Integrates easily into GitHub Actions, GitLab CI, Jenkins
  • Open-source: Free, transparent, and community-driven

Installation and setup

Installing Trivy

Terminal window
# macOS
brew install trivy
# Linux (Ubuntu/Debian)
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | tee -a /etc/apt/sources.list.d/trivy.list
apt-get update
apt-get install trivy
# Docker
docker pull aquasec/trivy

Basic image scanning

Terminal window
# Scan a local image
trivy image my-app:latest
# Scan from registry
trivy image nginx:latest
# Scan with detailed output
trivy image --severity HIGH,CRITICAL my-app:latest

Setting severity thresholds

Scanning at specific severity levels

Terminal window
# Only show critical and high severity issues
trivy image --severity CRITICAL,HIGH my-app:latest
# Exit with error code if vulnerabilities found
trivy image --severity HIGH,CRITICAL --exit-code 1 my-app:latest

Output formats

Terminal window
# JSON output for parsing
trivy image --format json my-app:latest
# SARIF format for GitHub integration
trivy image --format sarif my-app:latest
# Table format (default)
trivy image --format table my-app:latest

CI/CD integration

GitHub Actions workflow

.github/workflows/container-scan.yml
name: Container Vulnerability Scan
on:
push:
branches: [main]
paths:
- 'Dockerfile'
- 'src/**'
pull_request:
branches: [main]
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: false
load: true
tags: my-app:${{ github.sha }}
- name: Run Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: my-app:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Fail if critical vulnerabilities found
run: |
trivy image --severity CRITICAL my-app:${{ github.sha }} --exit-code 1

GitLab CI

.gitlab-ci.yml
stages:
- build
- scan
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
scan:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false

Policy enforcement

Creating a Trivy policy

trivy-policy.yaml
# Define what constitutes a vulnerability violation
severity: HIGH,CRITICAL
# Ignore specific CVEs for known/accepted risks
ignorefile: .trivyignore
# Policy for failing builds
exit-code: 1
# Require sign-off for medium severity
medium-requires-approval: true

Ignoring false positives

.trivyignore
# Format: CVE-XXXX-XXXXX [optional: expiration date]
# Known false positive or acceptable risk (expires 2026-12-31)
CVE-2024-1234 2026-12-31
# Permanently ignore (use with caution)
CVE-2024-5678

Beyond images

Scanning filesystems

Terminal window
# Scan local directory
trivy fs .
# Scan with detailed output
trivy fs --severity HIGH,CRITICAL --format json . > fs-scan.json

Scanning configuration files

Terminal window
# Detect misconfigurations in Dockerfile
trivy config Dockerfile
# Scan Kubernetes manifests
trivy config k8s-manifests/

Generating a Software Bill of Materials (SBOM)

Terminal window
# Generate SBOM in CycloneDX format
trivy image --format cyclonedx my-app:latest > sbom.xml
# Generate SBOM in SPDX format
trivy image --format spdx my-app:latest > sbom.spdx

Remediation

When vulnerabilities are found

  1. Smallest change: update the base image
# Before
FROM ubuntu:20.04
# After
FROM ubuntu:22.04
  1. Targeted change: update the vulnerable dependency
FROM node:18-alpine
# Install with security patches
RUN npm install --no-save my-package@latest
  1. Last resort: rebuild the image without cache
Terminal window
docker build --no-cache -t my-app:latest .

A scheduled scan across every image

.github/workflows/production-scan.yml
name: Production Container Security
on:
schedule:
# Run daily scans
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
scan-all-images:
runs-on: ubuntu-latest
strategy:
matrix:
image:
- my-app:latest
- api-gateway:latest
- worker-service:latest
steps:
- uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ matrix.image }}
format: 'json'
output: 'trivy-${{ matrix.image }}.json'
severity: 'CRITICAL,HIGH,MEDIUM'
- name: Archive results
uses: actions/upload-artifact@v3
with:
name: trivy-reports
path: trivy-*.json
- name: Notify security team
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Critical vulnerabilities found in ${{ matrix.image }}"}' \
${{ secrets.SLACK_WEBHOOK_URL }}

Monitoring and reporting

Storing results over time

Terminal window
# Generate timestamped reports
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
trivy image --format json my-app:latest > reports/scan-$TIMESTAMP.json
#!/bin/bash
# Count vulnerabilities by severity
trivy image --format json my-app:latest | \
jq '[.Results[]?.Vulnerabilities[]?.Severity] | group_by(.) | map({severity: .[0], count: length})'

Best practices

1. Scan early and often

  • Scan during development (local images)
  • Scan in CI/CD pipeline (before merge)
  • Scan in registry (continuous monitoring)
  • Scan in production (runtime detection)

2. Use minimal base images

# Reduce attack surface
FROM alpine:3.18 as base
FROM gcr.io/distroless/base-debian11

3. Update dependencies regularly

Terminal window
# Update dependencies regularly
npm audit fix --force
python -m pip install --upgrade pip

4. Keep SBOMs

Generate and store SBOMs for supply chain transparency:

Terminal window
trivy image --format cyclonedx my-app:latest > sbom.json
git add sbom.json
git commit -m "Update SBOM for security tracking"

Registry integration

Push only images that passed

Terminal window
# Only push if scan passes
trivy image --severity CRITICAL,HIGH --exit-code 1 my-app:latest && \
docker push my-registry/my-app:latest

Wrapping up

If you ship containers, something has to scan them before the registry does.

Trivy is the cheapest way I know to do that. Add it to the pipeline, pick the severity you’ll fail the build on, and treat the .trivyignore file as something that gets reviewed rather than something that grows. Start at CRITICAL if HIGH would block every build on day one, then tighten it once the base images are current.

Resources

Was this useful?

You might also enjoy

More posts on similar topics

Securing CI/CD with IAM Roles

Securing CI/CD with IAM Roles

Why secure your CI/CD pipeline? Why pipeline security matters Your pipeline holds credentials for every environment you deploy to, which makes it one of the most valuable targets you own. A s

Policy-as-Code Governance with OPA/Rego

Policy-as-Code Governance with OPA/Rego

Why policy-as-code matters The governance problem Managing infrastructure at scale gets complicated fast. As your infrastructure grows, keeping it consistent and compliant gets harder. M

GitHub Actions Secrets and Environment Variables: Handle Config the Right Way

GitHub Actions Secrets and Environment Variables: Handle Config the Right Way

Why secrets handling matters Most CI leaks are config mistakes, not attacks Hey, want to stop leaking credentials in your pipelines? Most secret leaks in CI are not the result of some cle

7 Reasons Learning the Linux Terminal is Worth It (Even for Beginners)

7 Reasons Learning the Linux Terminal is Worth It (Even for Beginners)

Why learn the Linux terminal? Why it still matters Even with the graphical tools and AI assistants available now, the terminal is the most direct way to work with a Linux system. It's a core

Docker Is Eating Your Disk Space (And How PruneMate Fixes It)

Docker Is Eating Your Disk Space (And How PruneMate Fixes It)

The problem: Docker is eating your disk space What it looks like when it happens Your Docker host is running out of space. Again. You've been spinning up containers, testing new services

Kubernetes Namespaces: Organize, Isolate, and Secure Multi-Team Clusters

Kubernetes Namespaces: Organize, Isolate, and Secure Multi-Team Clusters

Why cluster isolation matters The multi-tenant reality If you're running a separate cluster for every environment and every dev team, you have already seen the bill and the amount of upgrade

6 related posts