Cyber Intelligence
Supply Chain and Image Security · Supply chain

L16. Minimal Base Images: Distroless, Scratch and Chainguard

Video generating

Check back soon for the video lesson on Minimal Base Images: Distroless, Scratch and Chainguard

Every package in your container image is attack surface. Learn how to reduce image size and vulnerability count by using distroless, scratch, and Chainguard base images.

The Attack Surface Problem

A typical Ubuntu or Debian base image contains hundreds of packages: shells, package managers, compilers, utilities. Most of these are never used by your application but each one can contain vulnerabilities.

A standard python:3.12 image has 400+ packages and 100+ known vulnerabilities. A distroless equivalent has 20 packages and near-zero vulnerabilities.

Distroless Images

Google's distroless images contain only your application runtime and its dependencies. No shell, no package manager, no utilities:

FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

FROM gcr.io/distroless/python3-debian12 COPY --from=builder /app /app WORKDIR /app CMD ["main.py"]

Without a shell, an attacker who gains code execution cannot easily explore the filesystem, download tools, or establish a reverse shell.

Scratch Images

scratch is an empty image with literally nothing in it. It works for statically compiled binaries (Go, Rust):
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o /server .

FROM scratch COPY --from=builder /server /server COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ ENTRYPOINT ["/server"]

Scratch images have zero packages and zero vulnerabilities. The tradeoff: no shell for debugging and no DNS resolver (your application must handle DNS itself or copy the resolver libraries).

Chainguard Images

Chainguard provides hardened, minimalist images built with Wolfi (an undistro designed for containers). They include daily vulnerability patching and are signed with Sigstore:

FROM cgr.dev/chainguard/python:latest
COPY --chown=nonroot:nonroot . /app
WORKDIR /app
CMD ["main.py"]

Chainguard images run as non-root by default and include SBOMs. They cover most popular runtimes: Python, Node.js, Java, Go, .NET, and nginx.

Comparison

Base ImagePackagesCVEs (typical)ShellSize
ubuntu:24.04200+50-100Yes78 MB
python:3.12-slim100+20-40Yes130 MB
distroless/python3200-5No50 MB
chainguard/python150-2No45 MB
scratch00No0 MB

Multi-Stage Builds

All minimal image strategies use multi-stage builds:

  1. Builder stage: Install dependencies, compile code, run tests
  2. Runtime stage: Copy only the application artifacts into a minimal base image

This separation ensures build tools (compilers, package managers, test frameworks) never appear in the production image. Best practice: Pin base image versions to a specific digest rather than a mutable tag to prevent supply chain attacks through tag manipulation:

FROM cgr.dev/chainguard/python:latest@sha256:abc123...
Exam Focus Points
  • Every package in a container image is attack surface: minimal images drastically reduce vulnerability counts
  • Distroless images contain only the application runtime with no shell, package manager, or utilities
  • Scratch images are completely empty and work for statically compiled binaries (Go, Rust)
  • Chainguard images are hardened, run as non-root by default, include SBOMs, and receive daily vulnerability patches
  • Pin base image versions to a specific digest (not a mutable tag) to prevent supply chain attacks through tag manipulation
Knowledge Check

1. Why is the absence of a shell in distroless images a security benefit?

2. What type of applications can use scratch (empty) base images?

3. Why should you pin base images by digest instead of tag?