Yeda AI Tips · #166

Español

Secrets Deleted in a Later Docker Layer Still Live in History

You deleted the .env from your Docker image. It's still in there.

A Docker image isn't a single filesystem — it's a stack of read-only layers, one per build instruction. Every RUN, COPY, and ADD adds a new layer on top of the last. When a later layer "deletes" a file, it only writes a whiteout marker that hides the file in the merged view. The bytes from the earlier layer are never touched. The secret is still sitting there, one command away from anyone who has the image.

Why "delete" doesn't erase

Layers are immutable and content-addressed. This Dockerfile looks safe — it copies a credentials file, uses it, then removes it:

COPY .env /app/.env
RUN some-tool --config /app/.env && rm /app/.env

The rm runs in the same layer, so the merged image shows no .env. But if the secret was ever written to its own layer (a COPY .env, an ARG/ENV value, or a file created earlier and deleted later), that layer still contains it. A whiteout in a newer layer hides it; it does not delete it.

ENV and build ARG are worse: their values are stored in the image's metadata, readable straight from docker history or docker inspect — no unpacking required.

Extracting it takes one command

You don't need special tooling. Save the image to a tarball and look inside:

docker save myimage:latest -o image.tar
tar -xf image.tar          # each blob is a layer's filesystem

docker export (a running container) shows the merged view, so deleted files look gone. docker save exports every layer separately, so whiteout-hidden files reappear. Tools like dive surface the same thing interactively. Treat any image that ever touched a secret as already leaked.

The fix: never bake secrets into a layer

Anti-patternWhy it leaksUse instead
COPY .env then rmearlier layer keeps the fileRUN --mount=type=secret
ARG TOKEN=... / ENV TOKEN=...stored in image metadatabuild secret mount
download key, delete in later RUNearlier layer keeps the keybuild secret or runtime injection

BuildKit build secrets mount the value into the build container only for the duration of that one instruction — it never lands in a layer or in metadata:

docker build --secret id=aws,src=$HOME/.aws/credentials .
RUN --mount=type=secret,id=aws \
    AWS_SHARED_CREDENTIALS_FILE=/run/secrets/aws \
    aws s3 cp ...

For secrets your app needs at runtime (not build time), don't put them in the image at all — inject them at docker run via a secrets manager or orchestrator secret, so the image stays clean.

Power moves

Resources

Shipping AI or cloud systems? Yeda AI audits and hardens production LLM and container pipelines. Talk to us · Read the blog