Best Practices for SBOMs, Trivy Scans, and Automated CVE Mitigation in Docker Images¶
Introduction¶
Container security isn't optional. Every Docker image you ship is a potential attack vector. The question isn't whether to scan—it's how to scan systematically, generate Software Bill of Materials (SBOMs) for traceability, and automate remediation without breaking production.
Why SBOMs Matter:
SBOMs provide a complete inventory of software components in your images. They enable: - Supply chain traceability: Know exactly what's in every image - Vulnerability correlation: Map CVEs to specific components - Compliance: Meet regulatory requirements (SLSA, SOC2, ISO 27001) - Incident response: Quickly identify affected images when a CVE is disclosed
Why Trivy (and Similar Scanners):
Trivy is fast, accurate, and integrates well into CI/CD pipelines. It scans: - Container images (OS packages, language dependencies) - Filesystems and repositories - Infrastructure as Code (Dockerfiles, Kubernetes manifests) - Git repositories
But scanning alone isn't enough. You need workflows that automatically reduce vulnerabilities, not just detect them.
The Limitations of "Just Scan":
Running Trivy and failing builds on CVEs creates security theater, not security. Without remediation workflows: - Developers ignore failing builds - Critical vulnerabilities accumulate - Security debt compounds - Compliance becomes a checkbox exercise
This guide shows you how to build pipelines that scan, generate SBOMs, and automatically fix vulnerabilities in a controlled, auditable way.
SBOM Fundamentals for Docker Images¶
What is an SBOM?¶
A Software Bill of Materials (SBOM) is a machine-readable inventory of all software components in a container image. Think of it as a manifest that lists: - Operating system packages (apt, yum, apk) - Language dependencies (Python packages, npm modules, Go modules) - Base image layers - Build-time dependencies
Common Formats:
- CycloneDX: JSON/XML format, widely supported, good for tooling
- SPDX: ISO/IEC standard, comprehensive metadata, good for compliance
- Syft JSON: Syft's native format, easy to parse
Where SBOMs Fit:
- Build Time: Generate SBOM during image build, store as artifact
- Release Time: Attach SBOM to image tag, publish to registry
- Runtime/Deployment: Reference SBOM for vulnerability correlation, compliance audits
Inside Image vs Sidecar Artifact¶
Inside Image (SBOM as label/annotation): - Pros: Always travels with image, no separate artifact management - Cons: Increases image size, harder to update without rebuilding
Sidecar Artifact (SBOM as separate file): - Pros: Smaller images, can update SBOM without rebuilding, easier to version - Cons: Requires artifact management, risk of SBOM/image mismatch
Recommendation: Use sidecar artifacts stored alongside images in your registry or artifact repository. Reference SBOMs via image labels or annotations.
Tools & Ecosystem Overview¶
Trivy¶
Trivy is a comprehensive security scanner that: - Scans container images for OS and language vulnerabilities - Generates SBOMs in multiple formats (CycloneDX, SPDX) - Scans filesystems, Git repos, and IaC - Integrates with CI/CD pipelines - Provides JSON, SARIF, and HTML output formats
Why Focus on Trivy: - Fast scanning (parallel processing, caching) - Accurate results (comprehensive vulnerability databases) - Active development and community - Good documentation and examples - Free and open source
Alternative Tools¶
Syft + Grype (Anchore): - Syft generates SBOMs, Grype scans them - Good alternative if you prefer separate tools - Syft has excellent language dependency detection
Docker Scout: - Docker's native scanning tool - Integrated with Docker Hub and Docker Desktop - Good for Docker-native workflows
GitLab / GitHub Native Scanners: - Built into CI/CD platforms - Convenient but less flexible than Trivy - Good for getting started, consider Trivy for advanced workflows
Recommendation: Start with Trivy for maximum flexibility. Use platform-native scanners as secondary validation.
Generating SBOMs for Docker Images¶
Generating SBOMs from Images¶
Basic Trivy SBOM Generation:
# Generate CycloneDX JSON SBOM
trivy image --format cyclonedx --output sbom.cdx.json myimage:tag
# Generate SPDX JSON SBOM
trivy image --format spdx-json --output sbom.spdx.json myimage:tag
# Generate Syft JSON (using Syft directly)
syft myimage:tag -o json > sbom.syft.json
Tying SBOMs to Image Tags:
Use image labels to reference SBOMs:
# In Dockerfile
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL sbom.digest="sha256:abc123..."
LABEL sbom.format="cyclonedx"
LABEL sbom.location="s3://bucket/sboms/myimage-tag.cdx.json"
Or use OCI annotations:
# After building image
docker buildx imagetools create \
--annotation "org.opencontainers.image.sbom.digest=sha256:abc123" \
--annotation "org.opencontainers.image.sbom.format=cyclonedx" \
myimage:tag
Generating SBOMs at Build Time¶
Multi-Stage Build Pattern:
# Dockerfile
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
# Generate SBOM in build stage
FROM aquasec/trivy:latest AS trivy
COPY --from=builder /root/.local /tmp/deps
RUN trivy fs --format cyclonedx --output /tmp/sbom.cdx.json /tmp/deps
FROM python:3.11-slim
COPY --from=builder /root/.local /root/.local
COPY --from=trivy /tmp/sbom.cdx.json /sbom.cdx.json
ENV PATH=/root/.local/bin:$PATH
COPY app.py .
CMD ["python", "app.py"]
CI/CD Build-Time Generation:
# GitHub Actions example
- name: Build Docker image
run: docker build -t myimage:${{ github.sha }} .
- name: Generate SBOM
run: |
trivy image \
--format cyclonedx \
--output sbom.cdx.json \
myimage:${{ github.sha }}
- name: Upload SBOM artifact
uses: actions/upload-artifact@v3
with:
name: sbom
path: sbom.cdx.json
Storing SBOMs¶
Option 1: Artifact Repository (S3, GCS, Artifactory):
# Upload SBOM to S3 with versioned naming
aws s3 cp sbom.cdx.json \
s3://my-artifacts/sboms/myimage-${IMAGE_TAG}-${IMAGE_DIGEST}.cdx.json
# Store with metadata
aws s3 cp sbom.cdx.json \
s3://my-artifacts/sboms/myimage-${IMAGE_TAG}.cdx.json \
--metadata "image-digest=${IMAGE_DIGEST},image-tag=${IMAGE_TAG}"
Option 2: OCI Artifacts in Container Registry:
# Push SBOM as OCI artifact (Docker Hub, GHCR, Harbor support this)
oras push \
myregistry.io/myimage:${TAG} \
sbom.cdx.json:application/vnd.cyclonedx+json
# Or using Trivy's built-in support
trivy image --format cyclonedx \
--output sbom.cdx.json \
--sbom-artifact-type cyclonedx \
myimage:tag
Best Practices:
- Always generate SBOMs for release images: Tagged releases, not just dev builds
- Version SBOMs with image digest: Use
image-digestin SBOM filename or metadata - Include SBOM references in images: Use labels/annotations to link image to SBOM
- Store SBOMs immutably: Use content-addressable storage (digest-based naming)
- Generate multiple formats: CycloneDX for tooling, SPDX for compliance
Scanning Docker Images with Trivy¶
Core Patterns¶
Scanning Local Images:
# Basic scan
trivy image myimage:tag
# Scan with severity filter
trivy image --severity HIGH,CRITICAL myimage:tag
# Scan and fail on policy violations
trivy image --exit-code 1 --severity HIGH,CRITICAL myimage:tag
# Scan with JSON output
trivy image --format json --output scan.json myimage:tag
Scanning Remote Registry Images:
# Scan from Docker Hub
trivy image docker.io/library/nginx:latest
# Scan from private registry (with auth)
trivy image --username $REGISTRY_USER --password $REGISTRY_PASS \
myregistry.io/myimage:tag
# Scan from AWS ECR
trivy image --aws-region us-east-1 \
123456789012.dkr.ecr.us-east-1.amazonaws.com/myimage:tag
Scanning Dockerfiles:
# Scan Dockerfile for misconfigurations
trivy config Dockerfile
# Scan with policy checks
trivy config --severity HIGH,CRITICAL Dockerfile
Recommended Configurations¶
Severity Thresholds:
# Fail build on CRITICAL and HIGH only
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignorefile .trivyignore \
myimage:tag
Ignoring Specific CVEs:
Create .trivyignore file:
# .trivyignore
# CVE-2023-12345: False positive in our use case
# Justification: This CVE affects a feature we don't use
CVE-2023-12345
# CVE-2023-67890: Acceptable risk, will fix in next release
# Justification: Low severity in our deployment context, scheduled fix Q2 2024
CVE-2023-67890
Caching for Performance:
# Use cache directory (persist across CI runs)
trivy image --cache-dir /tmp/trivy-cache myimage:tag
# Or use Trivy DB cache
trivy image --cache-backend redis://redis:6379 myimage:tag
CI/CD Integration Examples¶
GitHub Actions (Basic):
name: Security Scan
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myimage:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myimage:${{ 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'
GitHub Actions (Advanced with SBOM):
name: Build, SBOM, and Scan
on:
push:
tags:
- 'v*'
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: myregistry.io/myimage:${{ github.ref_name }}
cache-from: type=registry,ref=myregistry.io/myimage:buildcache
cache-to: type=registry,ref=myregistry.io/myimage:buildcache
- name: Generate SBOM
run: |
trivy image \
--format cyclonedx \
--output sbom.cdx.json \
myregistry.io/myimage:${{ github.ref_name }}
- name: Upload SBOM
uses: actions/upload-artifact@v3
with:
name: sbom
path: sbom.cdx.json
- name: Scan image
run: |
trivy image \
--format json \
--output scan.json \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignorefile .trivyignore \
myregistry.io/myimage:${{ github.ref_name }}
- name: Upload scan results
uses: actions/upload-artifact@v3
with:
name: scan-results
path: scan.json
- name: Publish SBOM to registry
run: |
oras push \
myregistry.io/myimage:${{ github.ref_name }} \
sbom.cdx.json:application/vnd.cyclonedx+json
GitLab CI:
# .gitlab-ci.yml
stages:
- build
- security
build:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
security:
stage: security
image: aquasec/trivy:latest
script:
# Generate SBOM
- trivy image --format cyclonedx --output sbom.cdx.json $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# Scan image
- trivy image --format json --output scan.json --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# Upload artifacts
- |
if [ -f sbom.cdx.json ]; then
echo "SBOM generated successfully"
fi
artifacts:
reports:
sast: scan.json
paths:
- sbom.cdx.json
- scan.json
expire_in: 30 days
Output Management¶
Storing Reports:
# JSON for programmatic processing
trivy image --format json --output scan.json myimage:tag
# SARIF for GitHub/GitLab integration
trivy image --format sarif --output scan.sarif myimage:tag
# HTML for human-readable reports
trivy image --format template --template "@contrib/html.tpl" --output scan.html myimage:tag
Surfacing Results:
- CI Artifacts: Upload JSON/SARIF/HTML reports as build artifacts
- Security Dashboards: Integrate SARIF with GitHub Security, GitLab Security Dashboard
- Alerting: Parse JSON output, send alerts for new CRITICAL CVEs
- Compliance Reports: Generate periodic HTML reports for audits
Policies & Governance Around CVEs¶
Defining Risk Thresholds¶
Example Policy:
# security-policy.yaml
vulnerability_policy:
fail_build:
severity: ["CRITICAL"]
count_threshold: 0 # Fail on any CRITICAL
warn_build:
severity: ["HIGH"]
count_threshold: 5 # Warn if more than 5 HIGH severity
ignore_list:
file: .trivyignore
require_justification: true
review_required: true
Implementation:
# Fail on CRITICAL, warn on HIGH
trivy image \
--exit-code 1 \
--severity CRITICAL \
--ignorefile .trivyignore \
myimage:tag
# Generate report for HIGH (non-blocking)
trivy image \
--format json \
--severity HIGH \
--output high-severity-report.json \
myimage:tag
Managing False Positives¶
Process:
- Identify false positive: Verify CVE doesn't affect your deployment
- Document justification: Add comment in
.trivyignore - Review: Security team reviews ignore list entries
- Periodic audit: Re-evaluate ignored CVEs quarterly
Example .trivyignore with Justifications:
# CVE-2023-12345: False positive
# Component: libssl1.1
# Justification: CVE affects TLS 1.0/1.1, we only use TLS 1.2+
# Reviewed: 2024-01-15 by security-team
# Next review: 2024-04-15
CVE-2023-12345
# CVE-2023-67890: Acceptable risk
# Component: python3.9
# Justification: Low severity in containerized context, no network exposure
# Mitigation: Network policies restrict access
# Reviewed: 2024-01-20 by security-team
CVE-2023-67890
OS-Level vs App-Level CVEs¶
OS-Level CVEs: - Fixed by updating base image or running apt-get upgrade - Usually higher priority (affects entire container) - Mitigation: Regular base image updates
App-Level CVEs: - Fixed by updating language dependencies - May require code changes - Mitigation: Dependency update automation
Policy Example:
# Separate handling for OS vs app CVEs
# OS CVEs: Fail on CRITICAL
trivy image --severity CRITICAL --scanners os myimage:tag
# App CVEs: Warn on HIGH, fail on CRITICAL
trivy image --severity HIGH,CRITICAL --scanners lang myimage:tag
Maintaining Allowlists¶
Process:
- Request: Developer requests CVE addition to ignore list
- Justification: Require written justification
- Review: Security team reviews request
- Approval: Merge to
.trivyignorewith review comment - Audit: Quarterly review of all ignored CVEs
Automation:
# Validate .trivyignore format
trivy image --ignorefile .trivyignore --validate myimage:tag
# Generate report of ignored CVEs
trivy image --format json --ignorefile .trivyignore myimage:tag | \
jq '.Results[] | select(.Vulnerabilities != null) | .Vulnerabilities[] | select(.VulnerabilityID | IN($ignored[]))'
Designing Automated CVE Mitigation Workflows¶
7.1 Base Image Hygiene¶
Strategy: Regularly update base images and rebuild.
Best Practices:
- Use minimal base images:
-slim,distroless, oralpine - Pin to digests: Use
@sha256:...for reproducibility - Automate updates: Use Dependabot/Renovate to bump base images
- Rescan after updates: Automatically run Trivy on updated images
Example Workflow:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
Automated Base Image Update Flow:
flowchart LR
Bot["Dependabot/Renovate"] --> PR["Open PR: Update Base Image"]
PR --> Build["CI: Build Image"]
Build --> Scan["Trivy Scan"]
Scan --> Check{"CVEs<br/>Reduced?"}
Check -->|Yes| Merge["Auto-Merge PR"]
Check -->|No| Review["Human Review"]
Merge --> Tag["Tag New Image"] Implementation:
# GitHub Actions: Auto-merge if CVE count reduced
name: Auto-merge Base Image Updates
on:
pull_request:
paths:
- 'Dockerfile'
jobs:
check-cves:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build candidate image
run: docker build -t candidate:${{ github.sha }} .
- name: Scan current image
run: |
trivy image --format json --output current-scan.json \
myregistry.io/myimage:latest
CURRENT_CVES=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' current-scan.json)
echo "CURRENT_CVES=$CURRENT_CVES" >> $GITHUB_ENV
- name: Scan candidate image
run: |
trivy image --format json --output candidate-scan.json \
candidate:${{ github.sha }}
CANDIDATE_CVES=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' candidate-scan.json)
echo "CANDIDATE_CVES=$CANDIDATE_CVES" >> $GITHUB_ENV
- name: Auto-merge if improved
if: env.CANDIDATE_CVES < env.CURRENT_CVES
run: |
gh pr merge ${{ github.event.pull_request.number }} --auto --squash
7.2 Package-Level Updates in Dockerfiles¶
Strategy: Automatically bump OS packages and language dependencies.
OS Package Updates:
# Option 1: Update at build time (pros: latest packages, cons: non-deterministic)
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y python3 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Option 2: Pin versions, update via automation (pros: deterministic, cons: manual updates)
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y \
python3=3.11.2-1 \
python3-pip=23.0-1 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Language Dependency Updates:
Python (pip/poetry/uv):
# Use lock files for reproducibility
COPY requirements.txt poetry.lock ./
RUN pip install --no-cache-dir -r requirements.txt
# Or with poetry
RUN poetry install --no-dev --no-interaction
Automated Dependency Updates:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
versioning-strategy: increase
Node.js (npm/yarn):
Go Modules:
Guardrails:
- Use lock files:
requirements.txt,package-lock.json,go.sum - Automated PRs: Dependabot/Renovate create PRs for updates
- CI validation: Rebuild and rescan on dependency updates
- Test coverage: Ensure tests catch breaking changes
7.3 Autofix Loop with Scanner Feedback¶
Concept: Parse Trivy results, suggest Dockerfile changes, create PRs.
Workflow:
sequenceDiagram
participant CI as CI Pipeline
participant Trivy as Trivy Scanner
participant Parser as CVE Parser
participant Fixer as Autofix Engine
participant PR as PR Creator
CI->>Trivy: Scan image
Trivy->>Parser: JSON scan results
Parser->>Parser: Map CVEs to packages
Parser->>Fixer: CVE + package mappings
Fixer->>Fixer: Generate Dockerfile changes
Fixer->>PR: Create PR with fixes
PR->>CI: Trigger rebuild
CI->>Trivy: Rescan fixed image
Trivy->>CI: Verify CVEs reduced Implementation:
# scripts/autofix.py
import json
import subprocess
import re
from typing import Dict, List
def parse_trivy_results(scan_json: str) -> List[Dict]:
"""Parse Trivy JSON output and extract fixable CVEs."""
with open(scan_json) as f:
data = json.load(f)
fixable_cves = []
for result in data.get("Results", []):
for vuln in result.get("Vulnerabilities", []):
if vuln.get("Severity") in ["CRITICAL", "HIGH"]:
pkg_name = vuln.get("PkgName")
installed_version = vuln.get("InstalledVersion")
fixed_version = vuln.get("FixedVersion")
if fixed_version:
fixable_cves.append({
"cve_id": vuln.get("VulnerabilityID"),
"package": pkg_name,
"current": installed_version,
"fixed": fixed_version,
"severity": vuln.get("Severity"),
})
return fixable_cves
def generate_dockerfile_fix(dockerfile_path: str, fixes: List[Dict]) -> str:
"""Generate updated Dockerfile with package version fixes."""
with open(dockerfile_path) as f:
dockerfile = f.read()
# Map fixes to Dockerfile changes
for fix in fixes:
pkg = fix["package"]
old_ver = fix["current"]
new_ver = fix["fixed"]
# Pattern: apt-get install package=version
pattern = f"{pkg}={re.escape(old_ver)}"
replacement = f"{pkg}={new_ver}"
dockerfile = re.sub(pattern, replacement, dockerfile)
return dockerfile
def create_fix_pr(dockerfile_changes: str, fixes: List[Dict]) -> None:
"""Create PR with suggested fixes."""
# Write updated Dockerfile
with open("Dockerfile.fixed", "w") as f:
f.write(dockerfile_changes)
# Create PR via GitHub CLI or API
subprocess.run([
"gh", "pr", "create",
"--title", f"Security: Auto-fix {len(fixes)} CVEs",
"--body", generate_pr_body(fixes),
"--file", "Dockerfile.fixed",
])
if __name__ == "__main__":
fixes = parse_trivy_results("scan.json")
if fixes:
dockerfile_fixed = generate_dockerfile_fix("Dockerfile", fixes)
create_fix_pr(dockerfile_fixed, fixes)
CI Integration:
# GitHub Actions: Autofix workflow
name: Autofix CVEs
on:
schedule:
- cron: '0 0 * * 1' # Weekly
workflow_dispatch:
jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and scan
run: |
docker build -t testimage .
trivy image --format json --output scan.json testimage
- name: Run autofix
run: python scripts/autofix.py
- name: Create PR if fixes available
if: steps.autofix.outputs.has_fixes == 'true'
uses: peter-evans/create-pull-request@v5
with:
title: 'Security: Auto-fix CVEs'
body: 'Automated CVE fixes from Trivy scan'
branch: autofix/cves
Important: Autofix should create PRs for review, not mutate main branch directly.
7.4 Golden Base Images¶
Strategy: Maintain blessed base images, periodically update and scan.
Workflow:
flowchart TB
Base["Golden Base Image"] --> Update["Weekly Update Job"]
Update --> Build["Rebuild Base Image"]
Build --> Scan["Trivy Scan"]
Scan --> Check{"Passes<br/>Policy?"}
Check -->|Yes| Tag["Tag New Version"]
Check -->|No| Alert["Alert Security Team"]
Tag --> Promote["Promote to Golden"]
Promote --> App["Application Images Inherit"] Implementation:
# golden-base/Dockerfile
FROM debian:bookworm-slim
# Install common dependencies
RUN apt-get update && \
apt-get install -y \
curl \
ca-certificates \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Label with SBOM reference
LABEL org.opencontainers.image.title="Golden Base Image"
LABEL org.opencontainers.image.version="2024.01"
Golden Image Update Pipeline:
# .github/workflows/update-golden-base.yml
name: Update Golden Base Image
on:
schedule:
- cron: '0 0 * * 1' # Weekly
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build golden base
run: |
docker build -t golden-base:latest ./golden-base
- name: Generate SBOM
run: |
trivy image --format cyclonedx --output golden-base-sbom.cdx.json \
golden-base:latest
- name: Scan golden base
run: |
trivy image --format json --output golden-base-scan.json \
--exit-code 1 --severity CRITICAL \
golden-base:latest
- name: Tag and push if clean
if: success()
run: |
VERSION=$(date +%Y.%m.%d)
docker tag golden-base:latest \
myregistry.io/golden-base:${VERSION}
docker tag golden-base:latest \
myregistry.io/golden-base:latest
docker push myregistry.io/golden-base:${VERSION}
docker push myregistry.io/golden-base:latest
# Push SBOM
oras push myregistry.io/golden-base:${VERSION} \
golden-base-sbom.cdx.json:application/vnd.cyclonedx+json
Application Images Inherit Golden Base:
# Application Dockerfile
FROM myregistry.io/golden-base:latest
# Add application-specific dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
Example End-to-End CI/CD Pipelines¶
Pipeline 1: Standard Security Gate¶
Flow:
flowchart LR
Build["Build Image"] --> SBOM["Generate SBOM"]
SBOM --> Scan["Trivy Scan"]
Scan --> Policy{"Pass<br/>Policy?"}
Policy -->|Yes| Push["Push Image + SBOM"]
Policy -->|No| Fail["Fail Build"]
Push --> Registry["Registry"] Implementation:
# .github/workflows/security-gate.yml
name: Security Gate
on:
push:
branches: [main]
pull_request:
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build image
uses: docker/build-push-action@v4
with:
context: .
push: false
tags: myimage:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Generate SBOM
run: |
trivy image \
--format cyclonedx \
--output sbom.cdx.json \
myimage:${{ github.sha }}
- name: Scan image
run: |
trivy image \
--format json \
--output scan.json \
--exit-code 1 \
--severity CRITICAL,HIGH \
--ignorefile .trivyignore \
myimage:${{ github.sha }}
- name: Upload SBOM
uses: actions/upload-artifact@v3
with:
name: sbom
path: sbom.cdx.json
retention-days: 90
- name: Upload scan results
uses: actions/upload-artifact@v3
with:
name: scan-results
path: scan.json
retention-days: 90
- name: Push image and SBOM (on main)
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
docker push myimage:${{ github.sha }}
docker tag myimage:${{ github.sha }} myimage:latest
docker push myimage:latest
# Push SBOM as OCI artifact
oras push myregistry.io/myimage:${{ github.sha }} \
sbom.cdx.json:application/vnd.cyclonedx+json \
--username ${{ secrets.REGISTRY_USER }} \
--password ${{ secrets.REGISTRY_PASS }}
Pipeline 2: Autofix Loop¶
Flow:
flowchart TB
Schedule["Scheduled Job"] --> Update["Update Dependencies"]
Update --> Build["Build Candidate"]
Build --> Scan["Trivy Scan"]
Scan --> Compare["Compare CVE Counts"]
Compare --> Better{"CVEs<br/>Reduced?"}
Better -->|Yes| Merge["Auto-Merge PR"]
Better -->|No| Report["Generate Report"]
Merge --> Tag["Tag Image"]
Report --> Review["Human Review"] Implementation:
# .github/workflows/autofix-loop.yml
name: Autofix Loop
on:
schedule:
- cron: '0 2 * * 1' # Monday 2 AM
workflow_dispatch:
jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update dependencies
run: |
# Update base image (example: Dependabot would do this)
# Or run: pip install --upgrade -r requirements.txt
# Generate updated requirements.txt
pip-compile --upgrade requirements.in
- name: Build current image
run: |
docker build -t current:latest .
trivy image --format json --output current-scan.json current:latest
CURRENT_CVES=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' current-scan.json)
echo "CURRENT_CVES=$CURRENT_CVES" >> $GITHUB_ENV
- name: Build candidate image
run: |
docker build -t candidate:latest .
trivy image --format json --output candidate-scan.json candidate:latest
CANDIDATE_CVES=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' candidate-scan.json)
echo "CANDIDATE_CVES=$CANDIDATE_CVES" >> $GITHUB_ENV
- name: Create PR if improved
if: env.CANDIDATE_CVES < env.CURRENT_CVES
run: |
git config user.name "Autofix Bot"
git config user.email "autofix@example.com"
git checkout -b autofix/$(date +%Y%m%d)
git add requirements.txt Dockerfile
git commit -m "Security: Auto-fix CVEs (reduced from ${{ env.CURRENT_CVES }} to ${{ env.CANDIDATE_CVES }})"
git push origin autofix/$(date +%Y%m%d)
gh pr create \
--title "Security: Auto-fix CVEs" \
--body "Automated CVE fixes. Reduced HIGH/CRITICAL CVEs from ${{ env.CURRENT_CVES }} to ${{ env.CANDIDATE_CVES }}" \
--base main
- name: Generate report if not improved
if: env.CANDIDATE_CVES >= env.CURRENT_CVES
run: |
echo "CVE count not reduced. Current: ${{ env.CURRENT_CVES }}, Candidate: ${{ env.CANDIDATE_CVES }}"
trivy image --format template --template "@contrib/html.tpl" \
--output report.html candidate:latest
# Upload report or send alert
Integration with Container Registries & Artifact Repositories¶
Attaching SBOMs as OCI Artifacts¶
Docker Hub / GHCR:
# Push SBOM as OCI artifact
oras push \
ghcr.io/org/repo:tag \
sbom.cdx.json:application/vnd.cyclonedx+json \
--username $USERNAME \
--password $TOKEN
Harbor:
# Harbor supports OCI artifacts natively
oras push \
harbor.example.com/project/repo:tag \
sbom.cdx.json:application/vnd.cyclonedx+json
AWS ECR:
# ECR supports OCI artifacts
aws ecr put-image \
--repository-name myrepo \
--image-manifest "$(cat sbom.cdx.json | base64)"
Tagging Images with Security Posture¶
Labels for Security Metadata:
# In Dockerfile or via build args
LABEL security.scan.date="2024-01-15"
LABEL security.scan.tool="trivy"
LABEL security.scan.version="0.45.0"
LABEL security.cve.count.critical="0"
LABEL security.cve.count.high="3"
LABEL security.sbom.digest="sha256:abc123..."
LABEL security.sbom.format="cyclonedx"
Setting Labels in CI:
- name: Extract security metadata
run: |
SCAN_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
CRITICAL_COUNT=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' scan.json)
HIGH_COUNT=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity == "HIGH")] | length' scan.json)
docker build \
--label "security.scan.date=$SCAN_DATE" \
--label "security.cve.count.critical=$CRITICAL_COUNT" \
--label "security.cve.count.high=$HIGH_COUNT" \
-t myimage:tag .
Registry-Native Scanners¶
When to Use Registry Scanners:
- Convenience: Built into platform, no setup
- Compliance: Required by organizational policy
- Baseline: Good starting point
When to Use Trivy:
- Flexibility: More control over scanning policies
- CI/CD Integration: Better integration with build pipelines
- SBOM Generation: Native SBOM support
- Custom Policies: More granular control
Hybrid Approach:
Use both: - Trivy in CI/CD for build-time gates - Registry scanners for runtime monitoring and compliance reporting
Operational Best Practices¶
Regular Scheduled Scans¶
Scan Existing Images in Registry:
# .github/workflows/scan-registry.yml
name: Scan Registry Images
on:
schedule:
- cron: '0 0 * * 0' # Weekly
workflow_dispatch:
jobs:
scan-registry:
runs-on: ubuntu-latest
strategy:
matrix:
image:
- myregistry.io/app:v1.0
- myregistry.io/app:v1.1
- myregistry.io/app:latest
steps:
- name: Scan image
run: |
trivy image \
--format json \
--output scan-${{ matrix.image }}.json \
${{ matrix.image }}
- name: Check for new CRITICAL CVEs
run: |
CRITICAL_COUNT=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' scan-${{ matrix.image }}.json)
if [ "$CRITICAL_COUNT" -gt 0 ]; then
echo "::error::Found $CRITICAL_COUNT CRITICAL CVEs in ${{ matrix.image }}"
exit 1
fi
Reporting & Alerting¶
Slack Integration:
# scripts/alert_slack.py
import json
import requests
import os
def send_slack_alert(scan_json: str, webhook_url: str):
with open(scan_json) as f:
data = json.load(f)
critical_cves = []
for result in data.get("Results", []):
for vuln in result.get("Vulnerabilities", []):
if vuln.get("Severity") == "CRITICAL":
critical_cves.append({
"id": vuln.get("VulnerabilityID"),
"package": vuln.get("PkgName"),
"title": vuln.get("Title"),
})
if critical_cves:
message = {
"text": f"🚨 Found {len(critical_cves)} CRITICAL CVEs",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{len(critical_cves)} CRITICAL CVEs detected*\n" +
"\n".join([f"• {cve['id']}: {cve['package']}" for cve in critical_cves])
}
}
]
}
requests.post(webhook_url, json=message)
if __name__ == "__main__":
send_slack_alert("scan.json", os.getenv("SLACK_WEBHOOK_URL"))
Email Alerts:
# In CI pipeline
if [ "$CRITICAL_COUNT" -gt 0 ]; then
echo "Found $CRITICAL_COUNT CRITICAL CVEs" | \
mail -s "Security Alert: CRITICAL CVEs in $IMAGE" \
security-team@example.com
fi
Policy for Deprecating Images¶
Deprecation Workflow:
- Detect: Scheduled scan finds unresolved CRITICAL CVEs
- Alert: Notify security team and image owners
- Grace Period: 30 days to fix or justify
- Deprecate: Mark image as deprecated in registry
- Block: Prevent new deployments of deprecated images
Implementation:
# Mark image as deprecated
docker manifest annotate \
myregistry.io/myimage:tag \
--annotation org.opencontainers.image.deprecated=true \
--annotation org.opencontainers.image.deprecated.reason="CRITICAL CVEs unresolved"
# Or use registry API
curl -X PATCH \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"deprecated": true, "reason": "CRITICAL CVEs"}' \
https://registry.example.com/v2/org/repo/manifests/tag
Storing Historical SBOMs and Scan Results¶
Artifact Repository Pattern:
# Store with versioned paths
aws s3 cp sbom.cdx.json \
s3://artifacts/sboms/myimage/${IMAGE_TAG}/${IMAGE_DIGEST}.cdx.json
aws s3 cp scan.json \
s3://artifacts/scans/myimage/${IMAGE_TAG}/${SCAN_DATE}.json
Database Pattern:
-- Store scan results in database
CREATE TABLE image_scans (
id SERIAL PRIMARY KEY,
image_name VARCHAR(255),
image_tag VARCHAR(255),
image_digest VARCHAR(255),
scan_date TIMESTAMP,
critical_count INTEGER,
high_count INTEGER,
medium_count INTEGER,
low_count INTEGER,
scan_report JSONB,
sbom_location TEXT
);
CREATE INDEX idx_image_scans_image ON image_scans(image_name, image_tag);
CREATE INDEX idx_image_scans_date ON image_scans(scan_date);
Security & Compliance Considerations¶
Supply Chain Security Frameworks¶
SLSA (Supply-chain Levels for Software Artifacts):
- Level 1: Basic provenance (SBOMs provide this)
- Level 2: Version-controlled builds (CI/CD provides this)
- Level 3: Non-falsifiable provenance (signed SBOMs, attestations)
Mapping to SLSA:
- SBOMs: Provide software provenance (what's in the image)
- Scan Results: Provide security attestations
- Signed Artifacts: Use cosign or similar to sign images + SBOMs
Example: Signed SBOM:
# Generate and sign SBOM
trivy image --format cyclonedx --output sbom.cdx.json myimage:tag
cosign sign-blob --bundle sbom.cdx.json.bundle sbom.cdx.json
Compliance Expectations¶
SOC 2: - Control: Change management, vulnerability management - Evidence: SBOMs, scan reports, remediation workflows
ISO 27001: - Control: A.12.6.1 (Management of technical vulnerabilities) - Evidence: Vulnerability scanning, patch management, SBOMs
Traceability Requirements:
flowchart LR
Image["Docker Image"] --> Digest["Image Digest"]
Digest --> SBOM["SBOM"]
Digest --> Scan["Scan Report"]
SBOM --> Components["Component List"]
Scan --> CVEs["CVE List"]
Components --> CVEs
CVEs --> Remediation["Remediation History"] Implementation:
- Store image digest in SBOM metadata
- Link scan reports to image digests
- Maintain audit trail of all scans and fixes
Reproducibility¶
Ensuring Reproducible Builds:
- Pin base images: Use digests, not tags
- Lock dependencies: Use lock files (requirements.txt, package-lock.json)
- Deterministic builds: Set build timestamps, use build args
- Regenerate SBOMs: Same image should produce same SBOM
Example Reproducible Dockerfile:
# Pin base image to digest
FROM debian@sha256:abc123... AS base
# Use specific package versions
RUN apt-get update && \
apt-get install -y \
python3=3.11.2-1 \
&& \
apt-get clean
# Copy lock file
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
Limitations and Non-Goals¶
What Automation Cannot Safely Do¶
Arbitrary Auto-Edit of Application Code: - Don't automatically modify application source code - Don't change business logic to fix CVEs - Focus on dependency and base image updates only
Auto-Upgrade Major Versions: - Major version upgrades (Python 3.11 → 3.12) require testing - Breaking changes may require code modifications - Use automated PRs for review, not direct merges
Bypassing Security Policies: - Don't auto-ignore CVEs without review - Don't lower severity thresholds automatically - Maintain human oversight for security decisions
What You Still Need¶
Good Tests: - Comprehensive test suites catch breaking changes - Integration tests verify image functionality - Security tests validate fixes don't introduce regressions
Human Review: - Review autofix PRs before merging - Validate CVE ignore list justifications - Approve major version upgrades
Monitoring: - Monitor production for new vulnerabilities - Track CVE trends over time - Alert on critical CVEs in running containers
Conclusion¶
A sane maturity path for container security:
Phase 1: Generate SBOMs + Scan Images¶
Goals: - Generate SBOMs for all release images - Run Trivy scans in CI/CD - Store SBOMs and scan results
Implementation: - Add Trivy to CI/CD pipelines - Generate SBOMs at build time - Store artifacts in registry or S3
Phase 2: Enforce Basic Gating Policy¶
Goals: - Fail builds on CRITICAL CVEs - Warn on HIGH severity - Maintain ignore list with justifications
Implementation: - Configure Trivy exit codes - Create .trivyignore with reviews - Integrate scan results into CI gates
Phase 3: Introduce Golden Base Images¶
Goals: - Maintain blessed base images - Regular updates and scanning - Application images inherit golden bases
Implementation: - Create golden base image repository - Weekly update and scan pipeline - Update application Dockerfiles to use golden bases
Phase 4: Add Dependency Update Automation¶
Goals: - Automatically update dependencies - Validate updates with scans - Create PRs for review
Implementation: - Configure Dependabot/Renovate - CI validates dependency updates - Auto-merge safe updates
Phase 5: Add Autofix Proposal Loops¶
Goals: - Parse Trivy results programmatically - Generate Dockerfile fixes - Create PRs with suggested changes
Implementation: - Build autofix scripts - Integrate with CI/CD - Maintain human review process
Key Principles:
- Automate the boring: Dependency updates, base image bumps
- Review the risky: Major version changes, security policy changes
- Track everything: SBOMs, scans, remediation history
- Fail fast: Catch vulnerabilities early in CI/CD
- Fix systematically: Use automation to reduce manual toil
This approach transforms container security from a manual, error-prone process into a systematic, automated workflow that scales with your infrastructure.
See Also¶
- Docker & Compose - Production containerization patterns
- Ansible Security Hardening - Infrastructure security practices
- CI/CD Pipelines - Continuous integration patterns