Skip to main content

Docker Image Security Scanning — Trivy, Snyk, and ECR Scanning

Scan Docker images for CVEs and vulnerabilities using Trivy and Snyk, integrate scanning into CI/CD pipelines, and enforce policies that block unsafe images.

44 Terms

Overview and What You Will Learn

Every Docker image you pull from the internet contains an operating system, runtime, and libraries — all of which accumulate known vulnerabilities (CVEs) over time. Without scanning, you are running containers with known security holes in production. At Razorpay, where containers process financial transactions, a compromised container from an unscanned image with a critical vulnerability is a regulatory and business disaster.

In this guide you will learn how to scan images with Trivy (the industry standard, free, open-source), integrate scanning into GitHub Actions so no unscanned image reaches production, and understand how to interpret and act on scan results.

Core Principles

Bash
+------------------------------------------+
| Where vulnerabilities come from: |
| |
| Base image OS packages |
| ubuntu:22.04 has 200+ CVEs |
| alpine:3.18 has ~0 CVEs |
| |
| Language runtime packages |
| old node version = known CVEs |
| old python packages = known CVEs |
| |
| Your application dependencies |
| npm packages, pip packages |
| old lodash, old requests library |
+------------------------------------------+
|
v
+------------------------------------------+
| Trivy scans all three sources: |
| OS packages, language deps, app files |
| Returns: CVE ID, severity, fixed version |
+------------------------------------------+
|
v
+------------------------------------------+
| CI pipeline gate: |
| CRITICAL found -> fail build |
| HIGH found -> warn or fail |
| MEDIUM/LOW -> report only |
+------------------------------------------+

Detailed Step-by-Step Practical Lab

Milestone 1: Installing and Running Trivy
Bash
# macOS
brew install aquasecurity/trivy/trivy
# Linux (Debian/Ubuntu)
sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - 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 -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install trivy
# Linux (using the install script)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
# Verify installation
trivy --version
# Version: 0.48.0
# Scan a local image
trivy image payment-api:latest
# Sample output:
# payment-api:latest (alpine 3.18.4)
# Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
#
# Node.js (node-pkg)
# Total: 3 (LOW: 0, MEDIUM: 2, HIGH: 1, CRITICAL: 0)
#
# HIGH: lodash 4.17.20
# CVE-2021-23337 -- Command Injection Vulnerability
# Fixed Version: 4.17.21
Milestone 2: Understanding Scan Output
Bash
# Scan with table output (default) — human readable
trivy image nginx:1.25
# Scan with JSON output — for CI pipelines and automation
trivy image --format json nginx:1.25 > scan-results.json
# Scan with only CRITICAL and HIGH severity (ignore LOW/MEDIUM noise)
trivy image --severity CRITICAL,HIGH payment-api:latest
# Exit code reflects findings:
# 0 = no vulnerabilities found
# 1 = vulnerabilities found
# Use in scripts: trivy image myimage && echo "PASS" || echo "FAIL"
# Scan a remote image (pulls from registry)
trivy image registry.razorpay.in/payment-api:v3.1.0
# Scan with a timeout for large images
trivy image --timeout 10m large-image:latest
# Show only fixable vulnerabilities (where a patched version exists)
trivy image --ignore-unfixed payment-api:latest
# Only shows CVEs that have a fix available — more actionable
Milestone 3: Scanning Filesystem and Configs

Trivy scans more than just images:

Bash
# Scan the local filesystem for vulnerabilities in your code
trivy fs .
# Scans: package.json, requirements.txt, go.sum, Cargo.lock
# Finds vulnerabilities in YOUR dependencies before building the image
# Scan a Dockerfile for misconfigurations
trivy config Dockerfile
# Checks for: running as root, no HEALTHCHECK, using ADD instead of COPY,
# using :latest tags, missing USER instruction
# Scan docker-compose.yml
trivy config docker-compose.yml
# Checks for: missing resource limits, privileged containers,
# exposed sensitive ports, no health checks
# Scan infrastructure-as-code files
trivy config terraform/
trivy config k8s/
Milestone 4: GitHub Actions Integration

This is the most important milestone — blocking vulnerable images before they reach production:

YAML
# .github/workflows/build-scan-push.yml
name: Build, Scan, and Push
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: registry.razorpay.in
IMAGE_NAME: payment-api
jobs:
build-and-scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # Required for GitHub Security tab upload
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image (do not push yet)
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}
load: true # Load into local Docker daemon for scanning
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE_NAME }}:${{ github.sha }}
format: sarif # GitHub Security tab format
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1 # Fail the pipeline if CRITICAL/HIGH found
ignore-unfixed: true # Only fail on fixable vulnerabilities
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always() # Upload even if scan found issues
with:
sarif_file: trivy-results.sarif
- name: Login to registry (only if scan passed)
if: github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Push image (only if scan passed and on main)
if: github.ref == 'refs/heads/main'
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max

The pipeline guarantees: no image with CRITICAL or HIGH CVEs reaches the registry.

Milestone 5: ECR Enhanced Scanning

If you use AWS ECR, you get automatic scanning built in:

Bash
# Enable enhanced scanning on an ECR repository
aws ecr put-image-scanning-configuration \
--repository-name payment-api \
--image-scanning-configuration scanOnPush=true \
--region ap-south-1
# Get scan results for a pushed image
aws ecr describe-image-scan-findings \
--repository-name payment-api \
--image-id imageTag=v3.1.0 \
--region ap-south-1
# ECR Inspector (enhanced scanning) also scans:
# - OS packages
# - Programming language packages (npm, pip, etc.)
# - And continuously re-scans as new CVEs are discovered
# This means an image that was clean on push may show CVEs 3 months later
Milestone 6: Keeping Base Images Updated

Scanning is reactive — you find vulnerabilities after they exist. The proactive approach is keeping base images updated:

Bash
# Check which base image your Dockerfile uses
head -1 Dockerfile
# FROM node:20-alpine
# Pin to a specific digest for reproducibility
docker pull node:20-alpine
docker inspect node:20-alpine --format '{{index .RepoDigests 0}}'
# node@sha256:a84f9c2b1d3e...
# Update Dockerfile to pin by digest
# FROM node:20-alpine@sha256:a84f9c2b1d3e...
# This guarantees you always build from the exact same base
# But also means you have to update the digest when you want updates
# Use Renovate or Dependabot to automate base image updates:
# These tools open automated PRs when new base image versions are available
# Your CI pipeline scans the new image before merging
# Combine with: automated base image updates + CI scanning = always current

Trivy Configuration File

For consistent scanning across projects:

YAML
# .trivyignore — ignore specific CVEs that are not applicable
CVE-2023-12345 # Ignore — only affects PostgreSQL, we don't use it
CVE-2023-67890 # Ignore — only affects Windows builds
# trivy.yaml — default config (put in project root)
severity:
- CRITICAL
- HIGH
exit-code: 1
ignore-unfixed: true
timeout: 10m

Common Mistakes

Mistake Risk Fix
Scanning only on merge to main Vulnerable PRs pass review Scan on every PR, not just main
Not using --ignore-unfixed Many false positives from unfixable CVEs Add --ignore-unfixed to filter to actionable findings
Ignoring HIGH severity HIGH CVEs are frequently exploited Treat HIGH same as CRITICAL in production services
Never updating base images CVE count grows over time Use Renovate/Dependabot for automated base image PRs
Only scanning the final image Vulnerable dev dependencies not caught Also scan filesystem (trivy fs .) to catch dep vulnerabilities early

Troubleshooting Reference

Problem Cause Fix
Scan takes too long Large image with many packages Add --timeout 15m or scan in parallel with build
Too many false positives Unfixable OS CVEs Add --ignore-unfixed to filter to only fixable findings
Pipeline failing on LOW CVEs Wrong severity setting Use --severity CRITICAL,HIGH to ignore LOW/MEDIUM
ECR scan shows no results Scanning not enabled aws ecr put-image-scanning-configuration --scanOnPush true
Cannot scan private registry image Not authenticated docker login registry before running trivy
Tip

Add trivy fs . to your development workflow as a pre-commit check. Catching vulnerable npm packages or pip dependencies before building the image means you fix the problem at the source (your package.json) rather than just masking it by pinning a specific version.

Remember

A clean scan today does not mean a clean scan tomorrow. New CVEs are discovered daily. Configure ECR enhanced scanning or schedule a weekly trivy image run against your production images to catch CVEs that were not known when the image was first built.

Common Mistake

Setting exit-code: 0 in the Trivy GitHub Action to prevent pipeline failures, then treating scanning as a reporting exercise rather than a gate. If your scan never fails the build, it has no protective value. Set exit-code: 1 and enforce it — the short-term pain of fixing vulnerabilities is worth the long-term protection.

Security

At Zerodha and PhonePe, images with CRITICAL CVEs are blocked from production by CI pipeline enforcement. Engineers cannot bypass the scan by pushing directly to the registry — the deployment system only accepts images that were pushed through the CI pipeline, which means every deployed image has passed the scan gate. This is the correct model: make it structurally impossible to deploy an unscanned image.

Resources

Explore More in Docker Security and Production Practices

All 4 Topics

Frequently Asked Questions

Is Docker Image Security Scanning — Trivy, Snyk, and ECR Scanning free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Docker Image Security Scanning — Trivy, Snyk, and ECR Scanning topic cover?

Scan Docker images for CVEs and vulnerabilities using Trivy and Snyk, integrate scanning into CI/CD pipelines, and enforce policies that block unsafe images.