Container Security: Scanning Images in CI/CD

devops security

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:

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:

Metrics to Track

MetricTarget
Critical vulnerabilities0
High vulnerabilities< 5
Mean time to remediate< 7 days
Scan coverage100% 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.

All posts