Articles / Minimal and Distroless Base Images: Shrinking the Attack Surface
Containers · Base Images

Minimal and Distroless Base Images: Shrinking the Attack Surface

A container image built FROM ubuntu ships an operating system's worth of software to run one process. The shell, the package manager, coreutils, a dozen libraries your application never imports — all of it lands in production, all of it appears in your vulnerability scans, and all of it is available to an attacker who lands a shell. Minimal and distroless images invert that: you ship the application and its runtime dependencies, and nothing else.

The insight is old — attack surface is a function of installed code — but containers make it actionable in a way traditional servers never did. A VM accretes packages over years; a container image is rebuilt from a declarative recipe on every commit. That means you can choose, precisely and repeatably, what ends up inside.

The spectrum, from full OS to nothing

lands a shellAttacker withcode executionFat base imageshell, package manager,libsReady-made toolkit+ CVE footprint youshippedEscalate, fetch tools,move laterallyEvery unused packageis attack surface
Every package in the base image is toolkit an attacker inherits on landing.
  • Full distro (ubuntu, debian). Familiar, easy to debug, and the largest attack surface. Hundreds of packages, a shell, a package manager an attacker can use to pull tools.
  • Minimal distro (alpine, debian-slim). A fraction of the size. Alpine swaps glibc for musl, which occasionally breaks binaries but removes a lot of surface.
  • Distroless. Google's distroless images contain your app, its runtime libraries, CA certificates and timezone data — and no shell, no package manager, no ls. There is almost nothing for an attacker to pivot with.
  • scratch. The empty image. A statically-linked Go or Rust binary can run in it with literally nothing else present. The smallest possible surface, at the cost of zero on-box debuggability.

Distroless is the sweet spot for most teams: small and hard to live off, but still carrying the CA bundle and libc that real applications need. Projects like Chainguard's Wolfi push the same idea further, rebuilding minimal images frequently so they often ship with zero known CVEs on the day you pull them.

Why it matters: You cannot have a CVE in a package you did not install. Shrinking the base image cuts your patch backlog and your live attack surface in the same move — the vulnerability that isn't there needs no triage.

The 'no shell' objection, and the answer

The most common pushback is operational: 'How do I kubectl exec into a distroless container to debug it?' You don't — and that is the point, because neither can an attacker. The modern answer is kubectl debug with an ephemeral debug container, which attaches a throwaway image carrying your tools into the target pod's namespaces without baking those tools into the production image. Debuggability becomes something you attach on demand rather than ship permanently.

Getting there with multi-stage builds

The mechanism that makes distroless practical is the multi-stage build. A fat 'builder' stage compiles the app with its full toolchain; a lean final stage copies only the resulting artifact onto a distroless or scratch base. The compiler, the build dependencies, the intermediate files — none of it reaches production.

Multi-stage build landing on distrolessFROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

FROM gcr.io/distroless/static:nonroot
COPY --from=build /app /app
USER nonroot
ENTRYPOINT ["/app"]  # no shell, no apt, nothing to pivot with

Pin the base image by digest rather than a floating tag, so a rebuild is reproducible and an upstream change cannot silently alter what you ship. Combine that with a scanner in CI, and 'minimal' stops being a one-time cleanup and becomes a property the pipeline maintains.

Smaller images pull faster, cost less to store, and — the reason we are here — give an intruder almost nothing to work with. But the discipline only helps the images you actually build this way. The forgotten service still running a three-year-old full-distro image is the one carrying your unpatched CVEs, and you can't slim down what you haven't found.

Test for it — in practice
  • Take a production image and see whether it still carries a shell and package manager at all — if you can drop into an interactive prompt inside it, so can an attacker who lands code execution.
  • Glance at the FROM line of your top few images: a full-distro base (ubuntu, debian) is a far larger surface than a slim or distroless one.

That is a quick smell test on a couple of images; measuring surface across your whole registry and flagging the fat ones is what our assessment runs for you.

Keep reading
Secrets in Container Images and How They Leak