Most Docker image vulnerabilities trace back to four decisions made at build time, not to some exotic runtime exploit. Sysdig's 2022 Cloud-Native Security and Usage Report found that roughly 76% of containers in production run as root, despite USER directives and Kubernetes runAsNonRoot controls having existed for years. That number matters because of what happened in February 2019: CVE-2019-5736 showed that a malicious container running as root could overwrite the host's runc binary through /proc/self/exe, achieving arbitrary code execution on the host itself — not just inside the sandbox. Docker 18.09.2 and runc 1.0-rc7 patched the specific bug, but the underlying lesson survived the patch: a container is a process boundary, not a security boundary, and root inside one is still root next to a shared kernel. Add in oversized base images that multiply the CVE surface, and secrets accidentally baked into image layers that persist long after a RUN rm tries to delete them, and you have the four vulnerability classes that show up in nearly every image audit we run. This piece walks through each one and the concrete mitigation that closes it.
Why does running containers as root matter if they're already sandboxed?
It matters because containers share the host kernel, and root inside the container maps to root's kernel-level privileges unless you've explicitly remapped user namespaces. CVE-2019-5736 is the clearest proof: a container process with root access could write to /proc/self/exe, which pointed back at the host's runc binary, and the next docker exec on that host executed attacker-controlled code as host root. Docker and AWS both published guidance at the time recommending SELinux in enforcing mode, keeping the host's runc binary read-only, and — the mitigation that actually prevents the exploit class rather than one CVE — never running the containerized process as root in the first place. A Dockerfile ending in USER 1000 or a Kubernetes securityContext.runAsNonRoot: true doesn't stop every escape technique, but it removes the specific privilege escalation path that made CVE-2019-5736 practical, and it remains one of the highest-leverage, lowest-cost fixes available to any team building images today.
Why does base image choice change your CVE count more than anything you write?
Because every package in your base image is a package you now have to patch, whether your application uses it or not. A full ubuntu:22.04 or debian:bookworm base pulls in a package manager, shell utilities, and hundreds of libraries that exist for general-purpose use, not for your specific app — and each one is a potential CVE source scanners will flag. Minimal and distroless bases invert that math: Google's distroless project strips images down to your application and its runtime dependencies, removing shells, package managers, and most of the userland tooling attackers rely on for post-exploitation. Chainguard's minimal images take a similar approach, rebuilding common bases with a fraction of the packages and near-daily rebuilds against upstream CVE feeds. The practical effect shows up immediately in any scanner output: teams that move from a general-purpose base to a minimal or distroless equivalent routinely see their per-image finding count drop by an order of magnitude, simply because there's less installed software left to have vulnerabilities in.
Why do deleted secrets still show up in your image?
Because Docker image layers are immutable and additive — deleting a file in a later layer doesn't remove it from the earlier layer where it was written, it only hides it from the final filesystem view. If a RUN echo $API_KEY > /app/.env step is followed by a RUN rm /app/.env step, both commands still produced their own layer, and the secret is fully recoverable from the first one with docker history or by unpacking the image with docker save and inspecting the tarball layer by layer. This is architectural, not a bug — it's how content-addressable layer caching works — and it's why "we deleted it before the final stage" has never been a real mitigation. The fixes that actually work are multi-stage builds, where the secret only ever exists in an early build stage that's discarded and never becomes part of the final image, and BuildKit's --secret mount flag, which makes the secret available to a single RUN step as an in-memory mount that never touches a committed layer at all. Neither ARG nor ENV should ever carry a credential — both are trivially readable from image metadata even without unpacking layers.
What does an SBOM actually add once you're already scanning for CVEs?
An SBOM turns a point-in-time scan into a queryable inventory you can revisit the moment a new CVE drops, instead of re-pulling and re-scanning every image from scratch. A Software Bill of Materials in CycloneDX or SPDX format lists every package and layer that went into an image, along with resolved versions — so when a new base-image CVE is disclosed, a team with current SBOMs can answer "which of our running images are affected" in minutes by querying existing data, rather than triggering a fleet-wide rescan and waiting on results. This is precisely why continuous scanning and SBOM generation are typically paired rather than treated as separate exercises: the scan finds the vulnerability, and the SBOM tells you instantly everywhere else it applies.
How Safeguard Helps
Safeguard connects directly to registries including Docker Hub to generate a CycloneDX SBOM and run vulnerability scanning on every image, public or private, without requiring you to change your build pipeline first. Where this closes the loop past detection is self-healing containers: when continuous scanning flags a new CVE in a base image or a transitive dependency inside the image, Griffin generates a patch plan — an upstream version bump, a backport, or a substitution with a hardened Gold-registry equivalent — rebuilds the image, runs your existing test suite, and promotes the patched image under a new tag once it passes guardrails. Every self-healed image ships with an SBOM attestation, a plain-English Griffin explanation of what was fixed, and a signed provenance attestation, and rollback is one click if a readiness probe or policy check fails post-promotion. Across customer tenants, median time from CVE publication to patched production rollout runs 20-45 minutes, turning what used to be a manual PR-review-redeploy chain for base-image CVEs into a loop that mostly runs itself.