Docker's layer cache is what makes rebuilds fast: unchanged layers are reused instead of rebuilt. That same mechanism is also a recurring source of security incidents. A secret written into a layer stays in that layer's tarball even after a later layer "deletes" it, recoverable with docker history or by unpacking the image — and GitGuardian's State of Secrets Sprawl work has logged millions of secrets leaked publicly, container images among them. A cached base layer can go on shipping an unpatched CVE for weeks because the build never re-pulled it. And a shared CI cache can be poisoned by one branch and served to another. GitGuardian's research counted well over 12 million secrets exposed on public GitHub in a single year; cached image layers are a well-documented slice of that exposure. This guide walks through each caching risk and the fix that keeps the speed without the leak.
Risk 1: Secrets baked into a cached layer
The classic mistake is passing a credential as a build arg or copying a file in, using it, then deleting it. The delete happens in a later layer; the secret is still in the earlier one.
# WRONG — the token is permanently in layer history
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc \
&& npm ci && rm .npmrc
The fix is a BuildKit secret mount, which exposes the secret to a single RUN and never writes it to any layer.
# RIGHT — secret is mounted for one command only
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci --omit=dev
DOCKER_BUILDKIT=1 docker build \
--secret id=npmrc,src=$HOME/.npmrc \
-t myapp:secure .
Risk 2: Stale cached base layers hiding CVEs
If your FROM line uses a mutable tag and the cache already holds that base, a rebuild may reuse the old base instead of pulling the patched one — so you keep shipping a CVE that upstream fixed. Pull fresh base layers on a schedule and pin by digest so you know exactly what you cached.
# Force a fresh base pull for the security-sensitive rebuild
docker build --pull --no-cache-filter=base -t myapp:secure .
Risk 3: The apt-get update cache trap
Docker caches RUN apt-get update separately from RUN apt-get install. If update is cached and install changes, you install against a stale package index and may pull outdated, vulnerable versions. Always combine them in one layer.
# Combine so the index and install stay in sync
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Risk 4: Poisoned or leaky shared CI caches
Many CI systems share a build cache across branches or even repositories to speed things up. An attacker who can run a build on an untrusted branch can seed the cache with a malicious layer that a trusted build then reuses. Scope caches per repository and per protected branch, and never share a writable cache with untrusted pull requests.
# Scope the registry cache to a trusted ref, read-only for untrusted builds
docker buildx build \
--cache-from type=registry,ref=registry.example.com/myapp:cache-main \
--cache-to type=registry,ref=registry.example.com/myapp:cache-main,mode=max \
-t myapp:secure .
Caching risks and mitigations
| Risk | Root cause | Mitigation |
|---|---|---|
| Baked-in secret | credential written to a layer | BuildKit --mount=type=secret |
| Stale base CVE | mutable tag reused from cache | pin digest, periodic --pull |
| Outdated packages | apt-get update cached alone | combine update + install in one RUN |
| Cache poisoning | shared writable CI cache | scope cache per repo / protected branch |
| Leaked history | docker history recovers layers | scan built images, not just source |
How Safeguard scans your images
Layer caching problems are invisible in source but plain in the built artifact, which is why Safeguard scans the image itself, layer by layer. It detects secrets embedded in any layer — including ones a later layer tried to delete — flags base layers still carrying CVEs that upstream has patched, and inventories every component from the filesystem so a stale cached package cannot hide. Reachability analysis then narrows the vulnerability findings to those exploitable from code your service runs, and Safeguard opens an auto-fix pull request when a base-image bump or dependency patch closes a real gap. Run these checks in CI with the Safeguard CLI, gate pull requests with Secure Containers, and cover application dependencies with software composition analysis. The Safeguard vs Aqua comparison covers how secret and layer scanning fit alongside prioritized remediation.
Frequently Asked Questions
Can a secret really be recovered after I delete it in a later layer?
Yes. Each layer is stored as its own tarball, so the bytes written in an earlier layer survive even after a later layer removes the file. Anyone with the image can run docker history or unpack the layers and read it. Use a BuildKit secret mount so the value is never written to a layer at all.
Should I just disable the cache with --no-cache?
Not wholesale — that makes every build slow and does nothing about secrets that leak through a COPY. Use targeted controls instead: secret mounts for credentials, digest pins and periodic --pull for freshness, and scoped caches in CI. Reserve --no-cache for security-sensitive rebuilds where you need certainty.
Is layer caching safe to share across branches in CI?
Only for trusted, protected branches. A shared writable cache that untrusted pull requests can write to is a poisoning vector: a malicious branch seeds a layer that a trusted build later reuses. Scope caches per repository and per protected branch, and keep untrusted builds read-only against the shared cache.
How do I find secrets already baked into images I shipped?
Scan the built images, not just the source repository, because cached layers and vendored files can introduce secrets your source scan never sees. A layer-aware scanner walks each layer's contents and flags embedded credentials regardless of which layer added them.
Want to catch baked-in secrets and stale layers before they ship? Connect a repository at app.safeguard.sh/register, and read the image and secret scanning setup in the documentation at docs.safeguard.sh.