← All articles

Docker Images Are Leaking Your Secrets: How to Find and Fix Embedded Credentials

June 12, 2026

The Docker Layer Problem Nobody Talks About in Stand-Up

Your Git history gets a lot of attention when teams think about leaked secrets. Your .env files get a mention in onboarding docs. But Docker images? They quietly accumulate secrets across build layers, get pushed to registries, and sit there — sometimes publicly — while everyone assumes the container runtime is an abstraction layer that protects them.

It doesn't. This article walks through exactly how secrets end up in Docker images, how to inspect your own images for embedded credentials right now, and the build patterns that prevent this class of leak entirely.

How Secrets Get Baked Into Image Layers

Docker images are built from a stack of read-only layers, one per instruction in your Dockerfile. Each layer is a snapshot of the filesystem at that point. The critical thing to understand: deleting a file in a later layer does not remove it from an earlier layer. Anyone with access to the image can read every layer independently.

Here are the three most common ways secrets end up embedded:

1. ENV and ARG Instructions

Passing secrets as build arguments or environment variables is tempting because it feels like "not hardcoding" them. It isn't safe.

# Dangerous — ARG value is visible in image metadata
ARG DATABASE_URL
ENV DATABASE_URL=${DATABASE_URL}
RUN ./migrate.sh

Even if you never set ENV, the value passed to ARG during docker build --build-arg is recorded in the image's build history, visible via docker history --no-trunc.

2. COPY or ADD of Secret-Containing Files

# Copies .env into the image — including all secrets inside it
COPY .env /app/.env
RUN node scripts/seed.js
RUN rm /app/.env   # ← does NOT remove it from the layer above

The rm on the last line creates a new layer that hides the file from the running container's filesystem, but the layer containing .env still exists in the image and is readable by anyone who pulls it.

3. RUN Commands That Touch Credentials

# The token appears in build history
RUN curl -H "Authorization: Bearer ghp_XXXXXXXXXXXX" https://internal.api/setup

docker history --no-trunc <image> will show every RUN command verbatim. Tokens passed inline to curl, pip install --extra-index-url, or npm config set are all captured.

How to Inspect Your Images Right Now

You don't need a special tool to start. Run these commands against any image you own:

Check Build History

docker history --no-trunc <image-name>:<tag>

Scan the output for tokens, passwords, URLs with embedded credentials, or ARG/ENV assignments that contain secret-looking strings.

Inspect Layer Metadata

docker inspect <image-name>:<tag>

Look at the Env array in the output. Any environment variable set with ENV in the Dockerfile — including those derived from ARG — will appear here in plaintext.

Walk the Filesystem of Each Layer

Tools like Dive (open source) let you step through each layer interactively and inspect what files exist, even files "deleted" in later layers. This is the clearest way to see if a secrets file was ever COPYed in.

dive <image-name>:<tag>

Automated Scanning at the Registry Level

Manual inspection doesn't scale. For any team pushing more than a handful of images, scanning the source repo and build pipeline for secrets before they ever reach a Dockerfile is the right layer of defense. To catch what's already out there, run a free GhostCred scan across your repositories and surface exposed credentials that may be feeding your container builds.

The Right Patterns: Build-Time Secrets Without Embedding Them

Use Docker BuildKit Secret Mounts

BuildKit (enabled by default in Docker 23+) supports --mount=type=secret, which makes a secret available to a single RUN step as an in-memory tmpfs mount. It is never written to any layer.

# syntax=docker/dockerfile:1

# Build with:
# DOCKER_BUILDKIT=1 docker build --secret id=npmrc,src=$HOME/.npmrc .

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm install

The .npmrc file (containing your npm auth token) is available only during that RUN step and leaves no trace in the image.

Multi-Stage Builds to Isolate Secret Usage

Keep any stage that touches credentials separate, and copy only the artifacts — not the secrets — into the final image.

FROM node:20 AS builder
# secrets used here for private package installs
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

FROM node:20-slim AS runtime
# only the compiled output moves forward — no credentials
COPY --from=builder /app/dist /app/dist

Never Use ENV for Secrets That Need to Reach Production

Runtime secrets — database passwords, API keys, service tokens — should be injected at runtime by your orchestrator (Kubernetes Secrets, AWS Secrets Manager, HashiCorp Vault), not baked into the image as ENV values. The image itself should be credential-free and safe to store in any registry.

Hardening Your Registry

  • Audit public vs. private visibility. Check every repository in Docker Hub, ECR, GCR, or GHCR. A surprising number of teams accidentally publish images as public during initial setup and never change it.
  • Enable image scanning in your registry. ECR, GCR, and Docker Hub all offer built-in vulnerability scanning; some also flag secrets. Use it as a backstop, not a primary control.
  • Rotate any credential that ever appeared in an image. If inspection reveals a secret was baked into a layer — even a layer that was later "deleted" — treat it as compromised. Rotate immediately. The image may already have been pulled by a CI system, a developer's laptop, or a bad actor.
  • Tag and track your base images. Using node:latest makes it hard to audit what changed between builds. Pin to a digest (node:20-slim@sha256:...) for reproducible, auditable images.

A Quick Pre-Push Checklist

  1. Run docker history --no-trunc and grep for common secret patterns (key, token, password, secret, Bearer).
  2. Run docker inspect and review the Env array for plaintext secrets.
  3. Confirm no .env, *.pem, credentials, or config files containing secrets appear in any layer via Dive.
  4. Verify your CI pipeline uses BuildKit secret mounts or equivalent for any build-time credential access.
  5. Confirm the registry repository visibility is set to private unless you have an explicit reason for it to be public.

The Bottom Line

Container images are artifacts that travel far — CI pipelines, developer laptops, staging environments, production clusters, and sometimes public registries. Every secret baked into a layer travels with the image, indefinitely, to every destination it reaches. The fix isn't complicated, but it requires deliberate build hygiene: use BuildKit secret mounts for build-time credentials, inject runtime secrets via your orchestrator, and audit what's already out there before assuming you're clean.

See what's exposed in your own code.

Run a free scan