A developer copies a private key into a Docker image, uses it to download a dependency, and deletes it in the next line. The final container has no key at that path, so the image looks clean.
The key may still be inside the image.
Container images are built as layers. A later layer can hide or delete a file from the final filesystem view without erasing the bytes stored in an earlier layer. Anyone who can pull the image may be able to inspect those layers.
TL;DR
- Deleting a secret in a later Dockerfile instruction does not remove it from an earlier image layer.
ENVvalues persist in the image configuration, and build arguments can appear in history or provenance. Neither is a safe secret channel.- Use BuildKit secret mounts or SSH mounts so credentials exist only for the build step that needs them.
- Scan the full image and its metadata before publishing, not only the running container’s filesystem.
- If a real credential reached an image or registry, revoke and rotate it. Rebuilding the image does not invalidate the leaked secret.
Why This Matters
Container registries turn one build artifact into many copies: developer laptops, CI runners, deployment nodes, caches, mirrors, and backups. A credential baked into an image can therefore spread far beyond the original pipeline.
The registry does not have to be public. Anyone with image-pull permission may gain a secret that grants access somewhere more valuable, such as a source repository, package registry, cloud account, or production database. The image becomes a credential-distribution mechanism.
This is also easy to misunderstand. A normal docker run view shows the merged filesystem, not every historical layer or metadata field.
Where Secrets Hide
Files added in an earlier layer
This Dockerfile is unsafe:
FROM alpine:3.22COPY deploy-key /root/.ssh/id_ed25519RUN apk add --no-cache git && git clone git@example.org:team/private-app.git /appRUN rm -f /root/.ssh/id_ed25519COPY creates a layer containing the key. The final RUN creates a new layer that marks the path as deleted. It does not rewrite the old layer.
Combining copy, use, and deletion into one RUN instruction is still a poor secret-delivery design if the secret enters through COPY: the copy already happened in another layer and the build context may also be retained by tooling. Use a secret mount instead.
ENV and image configuration
An instruction such as ENV API_TOKEN=... persists in the image configuration and is visible through image inspection. Runtime environment variables also appear to processes and operational tooling with sufficient access. Environment variables are useful for non-secret configuration, but putting a value in the Dockerfile makes it part of the artifact.
Build arguments, history, and provenance
ARG is not the same as a final runtime environment variable, but Docker explicitly warns against using build arguments for secrets. Values may be exposed in image history or provenance attestations depending on how the build uses and records them. A pipeline masked variable does not become safe merely because it is passed to --build-arg.
Build logs and cache
Shell tracing, verbose package tools, failed commands, and debug output can print credentials. Build caches may preserve steps that used secret material incorrectly. Scanning only the pushed image will not find a token that was printed into a CI log.
Build Without Baking Secrets In
Docker BuildKit provides secret mounts. The secret is available to one RUN instruction without becoming part of the resulting layer.
# syntax=docker/dockerfile:1FROM alpine:3.22 AS buildRUN apk add --no-cache curlRUN --mount=type=secret,id=repo_token,required=true \ TOKEN="$(cat /run/secrets/repo_token)" && \ curl --fail --silent --show-error \ -H "Authorization: Bearer ${TOKEN}" \ https://packages.example.org/app.tar.gz \ --output /tmp/app.tar.gzSupply the secret at build time:
docker buildx build --secret id=repo_token,src=./repo-token.txt .Do not commit repo-token.txt; keep it outside the build context and source it from the CI platform’s protected secret store. For private Git access, prefer BuildKit’s SSH mount or supported Git-auth secret mechanism instead of copying a private key.
Multi-stage builds reduce what reaches the final image, but they are not a license to put secrets in early stages. Builders, caches, and exported intermediate stages may still expose them. Use secret mounts in every stage that requires credentials.
How to Inspect an Image
Start with metadata and layer history:
docker image inspect IMAGE:TAGdocker history --no-trunc IMAGE:TAGLook for credentials, internal URLs containing tokens, suspicious ARG or ENV values, and commands that echo secret variables. Then use a container-aware scanner that examines layers rather than only the merged filesystem. Run it against the same immutable digest that will be deployed.
For a controlled manual investigation, save the image to a tar archive in a restricted location and inspect the manifest and layer archives. Treat the extracted data as sensitive and delete it through your organization’s evidence or secure-cleanup process afterward. Do not upload a private image to a public analysis service.
Scanning is not proof of absence. Detectors miss unfamiliar formats, encoded values, split secrets, and credentials retrieved dynamically during a build. Review the Dockerfile, build configuration, CI logs, cache policy, and registry permissions alongside scanner results.
If You Find a Real Secret
Contain the credential before polishing the image:
- Revoke or disable the credential. Assume anyone with access to the image could have copied it.
- Determine scope. Identify registries, tags, digests, caches, CI artifacts, mirrors, nodes, and developer machines that received the image.
- Review use of the credential. Check the issuing system’s audit logs from before the image was first available.
- Fix the build. Replace
COPY,ARG, orENVsecret delivery with an appropriate secret or SSH mount. - Build a new image with no cache where necessary and scan its digest. Do not reuse a compromised credential during the rebuild.
- Remove vulnerable tags and artifacts according to registry retention policy. Remember that consumers may still retain local copies.
Deleting a registry tag does not revoke a credential and may not immediately remove underlying blobs. Rotation is the security boundary.
What You Can Do Today
- Search Dockerfiles for
ARG,ENV,COPY, andADDinstructions involving names such as token, key, secret, password, or credential. - Add Docker’s build checks and a layer-aware secret scan to CI before registry publication.
- Confirm that protected CI secrets are passed through BuildKit secret mounts, not build arguments.
- Scan a sample of existing production image digests, beginning with images that access private package or source repositories during build.
- Write a short response rule: any valid credential found in an image is rotated, not merely deleted from the Dockerfile.
The safe question is not “is the secret visible in the running container?” It is “did secret material ever become part of the build artifact or its records?”
Related Posts
- Kubernetes and Container Security: Attacks and Defenses — runtime and orchestration risks after the image is built.
- GitHub Secrets Management Is Still Failing — why repositories and automation keep leaking credentials.
- Minimum Package Age as a Supply-Chain Defense — reducing risk in the dependencies pulled into builds.
Sources
Useful read?
Find us again on Google.
Add Hive Security as a preferred source for practical security research and analysis.
Add as a preferred source on GoogleChoose Hive Security in Google's source preferences.