Skip to main content

Image Security and Supply Chain

Attackers don't always create new vulnerabilities -- they exploit known ones in your base images. A single unpatched dependency can give an attacker a foothold. This lesson covers how to choose secure base images, scan for vulnerabilities, and pin images for reproducibility.

Choose Minimal Base Images​

The more packages in your image, the larger the attack surface:

Base ImageSizeShell?Pkg Manager?Attack Surface
node:18~1 GB✓aptHigh -- hundreds of unused binaries
node:18-alpine~170 MB✓ (sh)apkMedium -- reduced surface
gcr.io/distroless/nodejs~130 MB✗✗Low -- runtime only
Start with Alpine

Alpine is a good default for most workloads. Move to Distroless if you can operate without a shell for debugging.

Use Multi-Stage Builds​

Keep build tools out of your final image:

# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build

# Stage 2: Runtime (no build tools)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER 1000
CMD ["node", "dist/server.js"]

The final image contains only the compiled application and production dependencies.

Scan for Vulnerabilities​

Scan every image before deploying. Never deploy an image with critical CVEs.

Scanning Tools​

ToolTypeCommand
Docker ScoutBuilt-indocker scout quickview my-app:1.0.0
TrivyOpen source, CI-friendlytrivy image my-app:1.0.0
SnykCommercial, fix suggestionssnyk container test my-app:1.0.0

Example: Trivy Scan​

# Scan a local image
trivy image my-app:1.0.0

# Or scan without installing Trivy
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image my-app:1.0.0

Example output:

Total: 34 (UNKNOWN: 0, LOW: 20, MEDIUM: 10, HIGH: 3, CRITICAL: 1)

┌───────────────┬────────────────┬──────────┬──────────────────┐
│ Library │ Vulnerability │ Severity │ Fixed Version │
├───────────────┼────────────────┼──────────┼──────────────────┤
│ openssl │ CVE-2024-XXXX │ CRITICAL │ 3.1.5-r0 │
│ curl │ CVE-2024-YYYY │ HIGH │ 8.5.0-r0 │
└───────────────┴────────────────┴──────────┴──────────────────┘

Action: If you see CRITICAL, update the affected package or base image before deploying.

Pin Images by Digest​

Tags are mutable -- nginx:latest can point to a different image each day. Pin production images by digest for immutability:

# Mutable (bad for production)
FROM node:18-alpine

# Immutable (good for production)
FROM node:18-alpine@sha256:8c5b2f3a1e2d...

Get the digest:

docker inspect --format='{{index .RepoDigests 0}}' node:18-alpine

Image Security Workflow​

Key Takeaways​

  • Use minimal base images (Alpine or Distroless) to reduce attack surface.
  • Use multi-stage builds to keep build tools out of the final image.
  • Scan every image for CVEs before deploying. Block critical findings.
  • Pin images by digest in production for immutability -- tags are mutable.
  • Re-scan images periodically. New CVEs are discovered after build time.

What's Next​