Container Security

Docker BuildKit Security Best Practices for 2026

BuildKit has been the default Docker builder for years, but its security features remain underused. Here are the practices that matter in 2026.

Yukti Singhal
Senior Researcher
6 min read

BuildKit replaced the legacy Docker builder in 2022 and has been the default for long enough that most production Dockerfile authors have used it without realizing. The feature set is significantly broader than the legacy builder's, and a meaningful share of that feature set is security-relevant: secret mounts that do not leak into layers, cache mounts that do not require rebuild churn, and native SBOM and provenance attestation generation. Adoption of the security-relevant features has lagged the underlying availability by years.

This post covers what BuildKit gives you in 2026, what the common mistakes still are, and the configuration patterns worth standardizing on.

Why are cache mounts the right way to handle package installs?

The traditional pattern for installing dependencies in a Dockerfile, running apt-get install or pip install -r requirements.txt, downloads the packages on every build that misses the layer cache. BuildKit's cache mounts, expressed with RUN --mount=type=cache,target=/var/cache/apt, persist the cache directory across builds without committing it to the layer. The result is faster builds and smaller images, and it eliminates a category of build-time exfiltration where a malicious dependency could write data to a cached location.

The security angle is more interesting than the performance angle. A cache mount is isolated from the image layers, so anything a build process writes to the mount is discarded when the layer is committed. This means a compromised package's post-install script cannot persist a backdoor in the mounted cache directory in a way that propagates to the final image. The script can still do whatever it wants during the build, and it can still write to the actual image filesystem, so cache mounts are not a security control on their own. But they do narrow the persistence surface, and they make the Dockerfile more analyzable because the mount declarations are explicit about which directories are ephemeral.

How do secret mounts actually work?

BuildKit's secret mounts, declared with RUN --mount=type=secret,id=foo, expose a secret to a specific RUN instruction without writing it to a layer. The secret is provided at build time through --secret id=foo,src=./path and is available inside the RUN as a file at /run/secrets/foo. When the RUN completes, the mount is torn down and nothing about it persists in the resulting image layer.

This is the right way to handle any credential needed at build time: npm tokens for private registries, pip extra-index credentials, SSH keys for cloning private repositories. The wrong way, which is still depressingly common, is ENV or ARG for the credential or COPY of a credential file. Both of these leak the credential into the image, where it can be recovered from any registry that hosts the image. The 2022 incidents where build-time credentials were recovered from public images on Docker Hub illustrated the cost; the affected organizations had to rotate the leaked tokens, which in some cases meant rotating npm publishing credentials with broader downstream impact.

What about SBOM and provenance attestations?

BuildKit can generate SBOM and SLSA provenance attestations natively, controlled by --sbom=true and --provenance=true on the build command. The output is attached to the image as an OCI artifact, signed if you have configured signing, and queryable through docker buildx imagetools inspect. This makes the supply chain metadata for an image available in the same registry as the image itself, without needing a separate SBOM pipeline.

The catch is that the SBOM is generated by BuildKit's own scanner, which is reasonable but not as deep as a purpose-built tool like Syft. For most images the BuildKit-generated SBOM is accurate enough to be useful; for images with unusual package managers or non-standard install patterns, the coverage gaps are real. The right pattern is to use BuildKit's native SBOM as the default, then run a deeper scan with Syft or equivalent in CI as a verification step. The two outputs should largely agree; when they disagree, the disagreement is informative.

How should you pin BuildKit's frontends and base images?

BuildKit Dockerfiles can specify a frontend with a syntax directive at the top of the file. The default frontend is reasonably stable, but using a specific syntax directive lets you opt into newer features and pin to a known version. The pin should be by digest, not by tag: # syntax=docker/dockerfile:1.6@sha256:.... The frontend is code that runs during the build, with access to the build context and the build environment; a compromised frontend would be a near-perfect supply chain attack, because every image built with it would be affected silently.

The same logic applies to base images referenced in FROM lines. Pin them by digest, monitor them through Renovate or equivalent, and treat the pin as a security-significant artifact. The 2024 incidents involving popular Docker Hub base images that received compromised rebuilds under the same tag, including the Alpine variant issues, demonstrated the failure mode where tag-only pinning silently picks up bad updates. Digest pinning combined with monitored updates is the right pattern; either alone is insufficient.

What are the BuildKit gotchas worth knowing?

The most common gotcha in 2026 is the interaction between BuildKit's cache and CI environments. BuildKit's local cache lives in /var/lib/buildkit or similar, and CI runners that share state across jobs without isolation can leak build artifacts from one project to another. The fix is to use the remote cache backends, with cache entries scoped per repository or per pipeline, rather than the default local cache. The --cache-to=type=registry,... and --cache-from=type=registry,... flags express this; the GitHub Actions and GitLab CI integrations have native cache scoping that does it for you.

The other gotcha is that BuildKit's caching can mask reproducibility problems. A build that succeeds because a cached layer happened to contain the right files, and that would fail without the cache, is not a reliable build. Run docker buildx build --no-cache periodically in CI to catch this, especially before any release that needs to be reproducible. The reproducibility check is also where you catch supply chain shifts: a build that suddenly pulls a different version of a dependency on a clean cache is a build that has had its inputs change, and that is worth knowing about.

How Safeguard Helps

Safeguard ingests BuildKit's native SBOM and provenance attestations as first-class inputs to the supply chain inventory, then runs deeper analysis on top. Reachability analysis tells you which of the dependencies in the image actually matter for the running service, so the prioritization is grounded in real risk rather than total package count. Griffin AI cross-references the image's base, its frontend, and its dependencies against our TPRM scoring and zero-day feed, surfacing the components that warrant review before promotion. Policy gates can block image promotion when the build pulls in a base or frontend that regresses your zero-CVE image baseline. The native BuildKit metadata is a starting point; the platform turns it into an enforceable control.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.