CoursesAdvanced container securityMinimal & secure images

Distroless & scratch

No shell, no package manager, nothing to pivot with.

Advanced14 min · lesson 6 of 25

A distroless image contains your application and its runtime dependencies — and deliberately nothing else. No shell, no package manager, no coreutils, no /bin at all. Google’s distroless images are the common source: a base/static image is a few megabytes with just CA certificates, tzdata, and libc. The security payoff is enormous: most post-exploitation depends on tools that are simply absent, so an RCE that would normally drop a shell finds there is no shell to drop.

Dockerfile
FROM gcr.io/distroless/base-debian12:nonroot
COPY --from=build /out/server /server
USER nonroot # distroless "nonroot" variant runs as uid 65532
ENTRYPOINT ["/server"]
# variants: static (fully static bins) · base (glibc) · cc (C deps) · java · python

What "no shell" changes

The absence of a shell reshapes the attacker’s options. There is no /bin/sh to exec, so shell-based payloads fail; no apt or apk to install tools; no curl or wget to pull a second stage; no cat, ls, or find to explore. An attacker who lands code execution in a distroless container is stuck with only what your binary itself can do — a dramatically smaller blast radius. It also means far fewer packages, so far fewer CVEs to track and patch.

terminal
$ docker run --rm gcr.io/distroless/static-debian12 sh
docker: Error response from daemon: exec: "sh": executable file not found in $PATH
$ docker run --rm gcr.io/distroless/static-debian12 ls
docker: Error ... "ls": executable file not found # nothing to live off

Debugging without a shell

The obvious objection — “how do I debug it?” — has two clean answers. Distroless ships :debug variants that add a BusyBox shell for troubleshooting, so you keep a shell-less image in production and run the debug tag when you need to poke. Better, use kubectl debug or docker debug ephemeral/attached containers to bring tools alongside the running container without ever baking them into the image. The production image stays inert.

terminal
# attach a throwaway toolbox to a running distroless container — image stays clean
$ docker debug svc-container # or: kubectl debug -it pod --image=busybox --target=app
# use the :debug tag only when you specifically need an in-image shell:
# FROM gcr.io/distroless/static-debian12:debug-nonroot
Distroless assumes a self-contained binary
Because there is no package manager, everything the app needs at runtime must already be in the artifact or the base variant — shared libraries especially. A dynamically linked binary on distroless/static will fail to start with a missing-loader error; match the variant to your binary (static for fully static, base/cc for glibc/C deps) or build static, as the next lesson covers.