Secrets in Container Registries: How Credentials Survive in Image Layers
July 5, 2026
Why Container Registries Are a Credential Graveyard
When a developer adds a secret to a Docker image — even temporarily, even in a step they later "delete" — that secret is preserved forever in the image's layer history. Push that image to a registry, and the credential is now reachable by anyone with pull access: every engineer on the team, every CI job, and any external actor who gains access to the registry.
This isn't a hypothetical. It's one of the most reliably reproducible credential leaks in modern infrastructure, and it's almost never caught by standard code review because the secret never touches a .js or .py file — it lives inside a binary image artifact.
How Secrets Get Baked Into Image Layers
Docker images are built as a stack of immutable layers, one per instruction in the Dockerfile. Each layer is a filesystem snapshot. The key thing to understand: removing a file in a later layer doesn't remove its data from an earlier layer. Anyone who inspects the image history or extracts individual layers can recover that data.
Here are the most common ways secrets end up persisted inside image layers:
1. COPY or ADD of a .env or config file
COPY .env /app/.env
RUN npm install
RUN rm /app/.env # ← This does NOT remove the secret from the layer above
The COPY instruction creates layer N. The RUN rm creates layer N+1. The file — and its contents — still exist in layer N and are trivially extractable.
2. ARG and ENV instructions at build time
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
ARG values are visible in docker history output if they appear in a RUN instruction. ENV values are stored persistently in the image manifest and visible to anyone who runs docker inspect <image>. Passing a secret in as a build argument and storing it in an environment variable is one of the most common sources of credential leaks in CI pipelines.
3. Credentials used during build-time RUN steps
RUN curl -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/org/private-repo/tarball > app.tar.gz
Even if the token is never written to a file, it appears in the layer metadata and is recoverable via docker history --no-trunc.
4. SSH keys or cloud credentials mounted naively
Copying an SSH private key or an AWS credentials file for use during a build step is a classic mistake. Even a single-line COPY id_rsa /root/.ssh/id_rsa followed by a later RUN rm leaves the key intact in the layer stack.
How to Inspect an Image for Secrets Right Now
You don't need special tools to verify this is a real risk. Run the following against any image you suspect:
Inspect environment variables
docker inspect <image> | jq '.[0].Config.Env'
Inspect build history (look for ARG values in RUN steps)
docker history --no-trunc <image>
Extract and search individual layers manually
# Save the image as a tarball
docker save <image> -o image.tar
# Extract it
mkdir image_layers && tar -xf image.tar -C image_layers
# Search every layer for common secret patterns
grep -r "AKIA" image_layers/ # AWS access key prefix
grep -r "ghp_" image_layers/ # GitHub personal access token prefix
grep -r "sk-" image_layers/ # OpenAI API key prefix
grep -rE "password\s*=" image_layers/
If you want this done automatically across your entire registry — including images pushed months ago — run a free GhostCred scan to surface exposed credentials mapped to your compliance posture in about 60 seconds.
Secure Patterns That Actually Prevent Layer Leakage
Use Docker BuildKit secret mounts
BuildKit (available since Docker 18.09) provides a first-class mechanism for passing secrets to build steps without writing them to any layer:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=github_token \
curl -H "Authorization: Bearer $(cat /run/secrets/github_token)" \
https://api.github.com/repos/org/private-repo/tarball > app.tar.gz
Build with:
DOCKER_BUILDKIT=1 docker build \
--secret id=github_token,src=./github_token.txt \
-t myapp:latest .
The secret is available only during that specific RUN step and is never written to the image filesystem or metadata.
Use multi-stage builds to discard build-time artifacts
Multi-stage builds let you keep the final image clean by copying only the compiled output from a builder stage — not the secrets used to produce it:
FROM node:20 AS builder
# Secrets used here never appear in the final image
COPY . .
RUN npm ci && npm run build
FROM node:20-slim AS runtime
COPY --from=builder /app/dist /app/dist
# Credentials from the builder stage are gone
Never pass secrets via ARG or ENV
Use build-time secrets (--mount=type=secret) or inject credentials at runtime via your orchestration platform (Kubernetes Secrets, AWS Secrets Manager, HashiCorp Vault). If a value absolutely must be available at runtime, inject it as an environment variable at container start, not at image build time.
Scan images before pushing to the registry
Integrate a secrets scanner as a step in your CI pipeline, before the docker push. Catching a leaked credential before it reaches the registry is vastly cheaper than rotating it after the fact.
Cleaning Up an Already-Pushed Image
If you've already pushed an image with a secret baked in, you need to act on two fronts simultaneously:
- Revoke the credential immediately. Assume it is compromised. Rotate the key, token, or password before doing anything else. Layer extraction is trivial for anyone with registry access.
- Delete the affected image tags from the registry. Most registries (Docker Hub, AWS ECR, GCR, GitHub Container Registry) allow tag deletion via the UI or API. Delete every tag that contains the affected layers, not just the latest one.
- Audit registry access logs. Check who or what pulled the affected image. Many cloud registries expose pull logs; review them for unexpected principals.
- Rebuild and repush from a clean Dockerfile. Verify with
docker inspectanddocker historythat the replacement image contains no sensitive values.
Compliance Implications: SOC 2 and HIPAA
Under SOC 2 CC6.1 (logical access controls) and HIPAA's Technical Safeguards (45 CFR §164.312), organizations are expected to limit access to sensitive data — including credentials — to authorized individuals. A container image in a shared registry that carries a hardcoded database password or cloud access key is a direct violation of the principle of least privilege, and it's the kind of finding auditors flag when they review artifact management practices.
Demonstrating that you scan images for secrets before pushing, and that you have a documented rotation procedure when a credential is found, is the kind of evidence that closes these findings efficiently.
Key Takeaways
- Deleting a file in a later Docker layer does not remove it from earlier layers. Assume any file ever copied into a build is permanently accessible.
ENVandARGvalues are visible in image metadata, not just the filesystem.- Use BuildKit secret mounts for any credential needed during a build step.
- Use multi-stage builds to ensure build-time secrets never exist in the final image.
- Scan images in CI before pushing. Rotate immediately if a secret is found in a previously pushed image.
See what's exposed in your own code.
Run a free scan