Container Security: Scanning Images in CI/CD
After Log4j and SolarWinds, everyone cares about supply chain security. Container scanning is a critical piece. Here’s how to integrate it.
The Problem
Your container image contains:
- Base OS with packages
- Application dependencies
- Your code
Any of these can have vulnerabilities:
FROM python:3.11 # CVEs in base image?
RUN pip install requests # CVEs in dependencies?
COPY app/ /app # CVEs in your code?
Scanning Tools
Trivy
Open-source, comprehensive, fast.
# Install
brew install aquasecurity/trivy/trivy
# Scan image
trivy image python:3.11
# Scan filesystem
trivy fs .
# Scan config
trivy config .
Grype
Anchore’s open-source scanner.
# Install
brew install grype
# Scan
grype python:3.11
Docker Scout
Docker’s native solution.
docker scout quickview python:3.11
docker scout cves python:3.11
Output Example
$ trivy image python:3.11
python:3.11 (debian bookworm)
Total: 127 (CRITICAL: 3, HIGH: 28, MEDIUM: 45, LOW: 51)
┌──────────────────┬────────────────┬──────────┬──────────────────────────┐
│ Library │ Vulnerability │ Severity │ Installed Version │
├──────────────────┼────────────────┼──────────┼──────────────────────────┤
│ openssl │ CVE-2023-XXXX │ CRITICAL │ 3.0.9-1 │
│ libcurl │ CVE-2023-YYYY │ HIGH │ 7.88.1-10 │
└──────────────────┴────────────────┴──────────┴──────────────────────────┘
CI/CD Integration
GitHub Actions
# .github/workflows/security.yml
name: Container Security
on:
push:
branches: [main]
pull_request:
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'table'
exit-code: '1'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
GitLab CI
# .gitlab-ci.yml
container_scan:
stage: test
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
- merge_requests
Jenkins
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'docker build -t myapp:${BUILD_NUMBER} .'
}
}
stage('Scan') {
steps {
sh '''
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
myapp:${BUILD_NUMBER}
'''
}
}
}
}
Handling Results
Fail on Critical
trivy image --exit-code 1 --severity CRITICAL myapp:latest
Allow Known Issues
# .trivyignore
CVE-2023-12345 # Accepted risk, no fix available
CVE-2023-67890 # False positive for our usage
Generate Reports
# HTML report
trivy image --format template \
--template "@contrib/html.tpl" \
-o report.html myapp:latest
# JSON for programmatic use
trivy image --format json -o results.json myapp:latest
# SARIF for GitHub Security tab
trivy image --format sarif -o results.sarif myapp:latest
Pre-Build Scanning
Scan your code before building:
Dependencies
# Python
trivy fs --scanners vuln requirements.txt
# Node
trivy fs --scanners vuln package-lock.json
# Go
trivy fs --scanners vuln go.sum
Dockerfile
# Check for misconfigurations
trivy config Dockerfile
SBOM Generation
Software Bill of Materials—inventory of everything in your image:
# Generate SBOM
trivy image --format spdx-json -o sbom.json myapp:latest
# Scan existing SBOM
trivy sbom sbom.json
Best Practices
1. Minimal Base Images
# Instead of
FROM python:3.11
# Use
FROM python:3.11-slim
# Or
FROM python:3.11-alpine
# Or
FROM gcr.io/distroless/python3
Fewer packages = fewer vulnerabilities.
2. Pin Versions
# Instead of
FROM python:3.11
# Use
FROM python:3.11.4-slim-bookworm@sha256:abc123...
Reproducible builds with known security posture.
3. Multi-Stage Builds
FROM python:3.11 AS builder
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt
FROM python:3.11-slim
COPY --from=builder /install /usr/local
COPY app/ /app
Build tools don’t ship in final image.
4. Run as Non-Root
FROM python:3.11-slim
RUN useradd -m appuser
USER appuser
COPY --chown=appuser:appuser app/ /app
Limits blast radius of container escape.
5. Regular Updates
# Scheduled scan in GitHub Actions
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM
New vulnerabilities are discovered constantly.
Policy Enforcement
Admission Controllers (Kubernetes)
Block vulnerable images from running:
# OPA Gatekeeper policy
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sImageVulns
metadata:
name: block-critical-vulns
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
maxCritical: 0
maxHigh: 5
Registry Scanning
Most registries support automatic scanning:
- Docker Hub
- GitHub Container Registry
- AWS ECR
- Google Artifact Registry
Metrics to Track
| Metric | Target |
|---|---|
| Critical vulnerabilities | 0 |
| High vulnerabilities | < 5 |
| Mean time to remediate | < 7 days |
| Scan coverage | 100% of images |
Final Thoughts
Container scanning isn’t optional anymore. The tools are free, integration is easy, and the alternative is finding out about vulnerabilities the hard way.
Add it to your CI/CD today.
Shift security left. Scan before you ship.