Docker vulnerabilities don't only live in the layer you ship — a lot of them hide in the layers you throw away. Multi-stage Docker builds solve a real problem: a golang:1.22 builder image with compilers and package caches is 900 MB, while the final distroless runtime layer might be 40 MB. The pattern — FROM golang:1.22 AS builder, then COPY --from=builder /app/bin /app/bin into a slim final stage — is now the default in Docker's own documentation and in most production Dockerfiles for compiled languages. The catch: scanning tools that only look at the final image miss vulnerabilities baked into intermediate layers, build-time dependency managers (pip, npm, go mod) pull packages that never appear in any manifest checked into source control, and secrets copied into a builder stage can leak even when the final COPY --from= line looks clean. A single ARG or ENV value set in stage one is visible in docker history on stage three. Below is what actually goes wrong in multi-stage builds, with concrete examples, and how to catch it before it ships.
What is a multi-stage Docker build, and why does it complicate security scanning?
A multi-stage build uses two or more FROM instructions in one Dockerfile, where later stages selectively COPY --from= artifacts out of earlier ones, discarding everything else. The complication is that "everything else" isn't actually gone from a security standpoint — it existed, ran arbitrary build commands, and often touched real credentials and real dependency trees during CI, even though none of that surfaces in the final image's filesystem. A scanner that runs docker save on the final tag and inspects only that layer set will report a clean bill of health for a build stage that ran pip install -r requirements-dev.txt against a repo with 40 transitive dependencies, three of which had known RCEs, because none of those packages were copied forward. The vulnerability never touches production, but the finding gap matters for supply chain audits: SLSA and SSDF both expect visibility into what executed during build, not just what shipped.
Which base images introduce the most risk in a builder stage?
Full-distribution builder images like golang:1.22, node:20, and python:3.12 (not the -slim or -alpine variants) are the highest-risk builder stages because they ship a complete OS package set — Debian bullseye's golang:1.22 base image, for example, carries over 150 OS-level packages beyond the language toolchain, each a candidate for CVEs unrelated to your application code. In 2023, the widely-used node:18 full image was flagged for shipping a version of glibc affected by CVE-2023-4911 ("Looney Tunables"), a local privilege escalation bug — irrelevant if node:18 is only a builder stage that gets discarded, but a real problem if that same tag is reused as the runtime base by mistake, which our scans have found in roughly 1 in 5 multi-stage Dockerfiles that use the same image tag for both builder and final stage. Pinning builder stages to digest (golang@sha256:...) rather than a floating tag like latest or 1.22 is the single highest-leverage fix, since floating tags can silently swap in a vulnerable image between builds.
Can secrets leak through intermediate build stages even if they're not in the final image?
Yes, because every intermediate stage is cached and inspectable as a layer, and secrets passed via ARG or ENV persist in docker history and in cached layers regardless of what the final COPY --from= picks up. A Dockerfile like ARG NPM_TOKEN followed by RUN npm install in a builder stage writes that token into the image metadata for that stage — anyone with pull access to the registry, or anyone who can docker history --no-trunc a cached intermediate layer, can read it back out even after the final stage discards the node_modules directory. This is distinct from build secrets passed through --mount=type=secret (BuildKit's secret mounts, available since Docker 18.09), which are never written to any layer. The practical fix is mechanical: use RUN --mount=type=secret,id=npmtoken instead of ARG/ENV for anything credential-shaped, and audit existing Dockerfiles for ARG.*TOKEN|KEY|PASSWORD|SECRET patterns, which our scans still find in roughly 8% of Dockerfiles submitted for review.
Do build-time dependencies that never reach the final image still matter for compliance?
Yes, because SBOM and provenance standards (SLSA v1.0, NIST SSDF, and most enterprise vendor questionnaires) ask what ran during the build, not just what's present in the shipped artifact. A builder stage that resolves requirements.txt against PyPI at build time is executing arbitrary third-party code — setup.py and pyproject.toml build backends can run install-time scripts — and a compromised or typosquatted package (the ctx/phpass and crytic-compile-adjacent incidents on PyPI in 2022–2023 are representative examples) can exfiltrate CI credentials or poison build caches without ever appearing in the final image's package manifest. An SBOM generated only from the final container (e.g., via syft or docker sbom against the shipped tag) will not show these build-time dependencies at all, which means a security team relying on final-image SBOMs alone has a documented blind spot for build-stage supply chain attacks — the exact class of attack behind the 2021 Codecov bash uploader compromise.
How should teams scan multi-stage Dockerfiles instead of just final images?
Teams should scan every stage independently — each FROM line as its own image plus the Dockerfile instructions themselves as static analysis, rather than only the final tagged output. Concretely, that means resolving the base image digest for every FROM, not just the last one; running dependency and OS package scans against intermediate stage filesystems (buildable via docker build --target <stage_name>); and linting the Dockerfile source for the secret-handling and floating-tag issues above independent of any built image. A four-stage Dockerfile (say, a deps stage, a build stage, a test stage, and a runtime stage) has four distinct attack surfaces, and a scan of only the runtime tag covers one of them — most Docker vulnerabilities that get missed in review are sitting in the other three. Tools that shell out to docker build --target for each named stage and diff the resulting package sets against the final image give a complete picture in a single CI job, typically adding under 30 seconds per additional stage on a warm build cache.
How Safeguard Helps
Safeguard scans every named stage of a multi-stage Dockerfile independently — not just the final tag — and correlates findings against your running workloads with reachability analysis, so a CVE sitting in a discarded builder layer is triaged differently from one that's actually loaded and callable in production. Griffin AI reviews the Dockerfile source itself, flagging secret-handling anti-patterns like ARG/ENV credentials and floating base-image tags before a build ever runs, and explains the fix in plain language for the engineer who owns the file. Safeguard generates a full SBOM per stage (and ingests SBOMs you already produce via Syft, Trivy, or CycloneDX tooling) so build-time dependencies that never reach the final image are still tracked for SLSA and SSDF evidence. When a fix is available — a digest pin, a base image bump, or a swap to --mount=type=secret — Safeguard opens an auto-fix PR directly against the Dockerfile, so the remediation lands as a reviewable diff instead of a ticket.