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
- Copied then deleted.
COPY id_rsa .to authenticate a build step,RUN rm id_rsaafterward. The key persists in the COPY layer regardless of the delete. - Build arguments. Values passed with
--build-argare visible in image metadata viadocker history, andARGvalues referenced in aRUNcan 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 candocker inspectthe image or runenvinside it. - The whole build context. A
COPY . .with no.dockerignorehoovers up.envfiles, cloud credentials in.aws/, and.git/history that may itself contain rotated-but-recorded secrets.
RUN git clone git@internal:app.git && \
rm /root/.ssh/id_rsa # too late — key is in the COPY layer forever
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.
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 soCOPY . .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.
- Run
docker historyon a build and read the layer commands and anyENVlines — credentials echoed or baked in there survive every later delete. - Check whether a repo has a
.dockerignoreat all; aCOPY . .with none is a strong sign.envor.gitmay 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.