"Use a minimal base image" has been container security advice for a decade. It's not wrong, but teams treat it as the dominant lever, and it isn't. A distroless image running as root with CAP_SYS_ADMIN, a writable root filesystem, no seccomp profile, and host networking is dramatically more dangerous than a full Debian base running non-root with a RuntimeDefault seccomp filter and a read-only root FS. Base image size is one signal. It is not the signal.
This post walks through where the minimal-image myth breaks down in practice and what the actual attack-surface drivers are — the ones CIS benchmarks, NIST SP 800-190, and post-incident reports keep returning to.
Is Alpine actually safer than Debian slim?
Not reliably, and sometimes measurably worse. Alpine's attraction is a ~5 MB image built on musl libc and BusyBox, against ~75 MB for debian:stable-slim. Smaller surface, fewer binaries, fewer CVEs on paper. The problem is that musl is not a drop-in glibc replacement, and the difference leaks into security-relevant places. DNS resolution behaves differently — musl historically did not honor search domains the same way glibc does, and several DNS-exfiltration detection tools rely on glibc's resolver semantics. Python wheels for numerical libraries require the musllinux variant, which gets CVE patches on a lag behind manylinux. Go binaries built with CGO_ENABLED=1 on an Alpine builder and deployed elsewhere produce subtle runtime crashes that get triaged as availability issues but mask real bugs.
More directly: Alpine's apk package manager has historically shipped a smaller security tooling set and slower advisory feed than Debian's. When CVE-2021-36159 hit libfetch in 2021, Alpine images without patched apk versions were exploitable through a simple apk add on a compromised mirror. Debian had a functioning security tracker and patched packages within hours. A smaller base is only safer if the distribution behind it keeps pace with vulnerability disclosure, and that is a separate question from image size.
Does distroless mean zero CVEs?
No. Distroless means no shell and no package manager — it does not mean no vulnerable code. The application binary, its static or dynamic dependencies, the language runtime (if present), and the CA certificate bundle are all still in the image and still scannable. A Go service built into gcr.io/distroless/static:nonroot will show zero CVEs from the base, but the compiled binary can contain a vulnerable golang.org/x/net version, a vulnerable github.com/golang-jwt/jwt/v4 below 4.5.1, or a vulnerable protobuf release — and those are the CVEs an attacker actually uses.
Distroless' genuine benefit is post-exploitation. If a process escapes into the container, there is no /bin/sh, no curl, no wget, no apt. Attackers can — and increasingly do — bring their own statically-linked tooling via the initial RCE payload, but that is more work and produces more signal for detection. The absence of a shell raises the cost; it does not remove the vulnerability that let the attacker in.
What are the real attack-surface drivers?
Six things, in rough order of impact. Linux capabilities — a container with CAP_SYS_ADMIN, CAP_NET_ADMIN, or CAP_SYS_MODULE can do damage no base image choice will prevent. The user the container runs as — USER 0 (root) inside the container is only safe if everything outside the container is also configured correctly, and in practice it rarely is. The root filesystem mount mode — a read-only root FS with tmpfs-mounted writable paths blocks the majority of persistence and tampering payloads. Seccomp and AppArmor profiles — Kubernetes' RuntimeDefault seccomp blocks roughly 60 syscalls including ptrace, mount, and kexec_load, which eliminate entire exploit classes regardless of what's in the image.
Then network policy — a workload that can reach the cluster API server, the cloud metadata endpoint at 169.254.169.254, or arbitrary egress destinations has a materially larger attack surface than one locked to its required services. And finally mount sources — hostPath mounts, especially to /var/run/docker.sock or /proc, convert container-level compromise into node-level compromise, and that conversion does not care how minimal the base image was.
Is Chainguard's "low CVE" marketing meaningful?
Partially, and mostly for the right reasons. Chainguard Images, built on Wolfi, ship with aggressive patch cadence, SBOM and signed attestations out of the box, and a deliberately stripped package set. The CVE counts are genuinely lower than public distro bases because packages are updated faster and the set is smaller. For teams that want FIPS-validated variants or hard SLAs on patching, the product is real.
What it does not do is substitute for the runtime controls above. A Chainguard base in a Pod with privileged: true, host network, and a service account bound to cluster-admin is a cluster takeover waiting to happen. The CVE count on the image is close to zero; the risk is close to maximum. The marketing is accurate in a narrow frame and actively misleading if teams use it to skip the other controls.
What does a defensible container security baseline look like?
Start with the controls that don't depend on base-image choice. Run as a non-root UID above 10000 with a fixed fsGroup. Set readOnlyRootFilesystem: true and enumerate the writable paths explicitly via emptyDir or tmpfs mounts. Drop all capabilities and add back only what you need — most workloads need none, a few need NET_BIND_SERVICE to bind to port 80/443, almost nothing legitimate needs SYS_ADMIN. Apply seccompProfile: { type: RuntimeDefault } at minimum; custom profiles for high-risk workloads. Set allowPrivilegeEscalation: false and privileged: false. Enforce these via Kyverno, OPA Gatekeeper, or Pod Security Admission' restricted profile.
Then pick the base image. For most services, distroless/static:nonroot or the equivalent Chainguard variant is a good default. For applications that need a shell and a package manager at runtime (they mostly shouldn't), debian:stable-slim or ubuntu:latest with disciplined patching are fine. Alpine is defensible for workloads where musl-vs-glibc differences are known and tested, and for CI builder images where size matters more than runtime behavior.
Does image size still matter at all?
Yes, just not for the reason most teams think. A smaller image pulls faster, which matters for cold-start latency on serverless and autoscaling workloads. A smaller image has less code to audit and fewer CVE candidates for scanners to evaluate, which reduces triage load even if it does not directly reduce exploitable risk. And a smaller image with a narrower package set gives cleaner SBOM output and fewer false positives on reachability analysis. Those benefits are real and cumulative. They just don't make the container secure by themselves.
How Safeguard.sh Helps
Safeguard.sh's reachability analysis evaluates whether a CVE in your base image — Alpine, Debian, distroless, or Chainguard — actually intersects with code your application loads at runtime, cutting 60-80% of the noise teams get from "scan the whole image" tools. Container self-healing catches drift and rolls workloads back when runtime signals — syscall anomalies, unexpected outbound connections, or capability-elevation attempts — indicate a compromise that base-image minimalism cannot prevent. Griffin AI generates pull requests that patch both the image layer and the Kubernetes manifest at once, enforcing non-root, read-only root FS, and RuntimeDefault seccomp alongside the CVE fix. SBOM generation captures the full layered package set and the application binary's transitive dependencies at 100-level depth, and the TPRM module extends the same controls to vendor-supplied container images before they reach your registry.