Articles / Secrets in Container Images and How They Leak
Containers · Secrets

Secrets in Container Images and How They Leak

A container image is not a single filesystem — it is a stack of layers, each a diff over the one below, and each preserved forever in the image's history. That architecture is what makes image caching fast and rebuilds cheap. It is also why a credential you copied in during a build and deleted two lines later is still sitting in the registry, fully recoverable, in the layer where you added it.

The mental model that leaks secrets is treating a Dockerfile like a shell script, where a later rm undoes an earlier cp. It doesn't. Each instruction that changes the filesystem creates a new layer; deleting a file in layer 7 simply records, in layer 7, that the file is gone — the bytes still live in layer 5. Anyone who pulls the image can unpack the layers and read it.

The four ways secrets get baked in

delete ≠ removalpushBuild stepCOPY secret into a layerImage layerstores the bytes foreverLater RUN rmjust records a deletionRegistryimage + full historyAnyone who can pullunpacks the layerSecret persists inthe COPY layerA published,recoverablecredential
A secret copied into a layer stays there — a later delete never removes it.
  • Copied then deleted. COPY id_rsa . to authenticate a build step, RUN rm id_rsa afterward. The key persists in the COPY layer regardless of the delete.
  • Build arguments. Values passed with --build-arg are visible in image metadata via docker history, and ARG values referenced in a RUN can end up in that layer's command record.
  • Baked-in environment variables. ENV DATABASE_PASSWORD=... is stored in the image config in cleartext and is readable by anyone who can docker inspect the image or run env inside it.
  • The whole build context. A COPY . . with no .dockerignore hoovers up .env files, cloud credentials in .aws/, and .git/ history that may itself contain rotated-but-recorded secrets.
Looks clean, leaks the keyCOPY deploy_key /root/.ssh/id_rsa
RUN git clone git@internal:app.git && \
    rm /root/.ssh/id_rsa  # too late — key is in the COPY layer forever
Why it matters: A secret in an image layer is not a secret. It is a published credential with a distribution channel — your registry — and it survives every subsequent 'delete'. Rotate anything that has ever been built into an image.

Building without leaving a trace

The right primitive is a build-time secret mount that never becomes a layer. BuildKit's --mount=type=secret exposes a secret to a single RUN instruction as a file under /run/secrets, present only for that command and absent from the resulting image and its history. The build authenticates; the artifact stays clean.

BuildKit secret mount — present during the step, gone from the imageRUN --mount=type=secret,id=npmtoken \
  NPM_TOKEN=$(cat /run/secrets/npmtoken) npm ci
# build with: docker build --secret id=npmtoken,src=./token.txt .

Keeping runtime secrets out of the image entirely

Build secrets are only half the problem; the other half is the credentials the app needs at runtime. Those should never be in the image at all. Inject them at deploy time — as mounted Kubernetes Secrets, or better, fetched at startup from an external store such as Vault or a cloud secrets manager — so the image remains a pure, shareable artifact and the credential's lifecycle is decoupled from the build's.

  • Scan images for secrets in CI. Tools like Trivy, Gitleaks and TruffleHog detect embedded keys before an image reaches a registry; fail the build on a hit.
  • Use a .dockerignore. Exclude .git, .env, credential directories and local config so COPY . . cannot pull them in.
  • Assume exposure is permanent. If a secret ever entered a pushed image, rotate it — do not just rebuild. The old image tag can still be pulled.

Secret hygiene in images is ultimately an inventory problem in disguise: you have to know which images exist, where they are stored, and what went into each. The credential you can't account for is the one already sitting in a layer in a registry you forgot you had.

Test for it — in practice
  • Run docker history on a build and read the layer commands and any ENV lines — credentials echoed or baked in there survive every later delete.
  • Check whether a repo has a .dockerignore at all; a COPY . . with none is a strong sign .env or .git may have been pulled into an image.

This catches the obvious cases; deep layer-by-layer secret discovery across every image and tag you have ever pushed is what our assessment runs for you.

Keep reading
Secrets Management in Kubernetes: etcd Encryption and External Stores