Secrets in Images

Secrets Management: What Not to Bake Into an Image

An image is a public document: anyone who can pull it can read every layer, its history and its configuration. ENV values sit in the configuration, build arguments used by a RUN are recorded in the history, and a file deleted in a later layer still exists in the earlier one. A small image shows all of it:

How secrets leak into an image, and a linter that noticesShell
printf '%s\n' 'FROM alpine:3' 'ARG NPM_TOKEN' 'ENV API_KEY=sk-demo-0123456789' \
  'RUN echo "_authToken=${NPM_TOKEN}" > /root/.npmrc && rm /root/.npmrc' > leaky.Dockerfile
docker build -q -f leaky.Dockerfile -t l3-leaky --build-arg NPM_TOKEN=npm_demo_abc123 . \
  >/dev/null
docker history --no-trunc --format '{{.CreatedBy}}' l3-leaky \
  | grep -oE '(NPM_TOKEN|API_KEY)=[^ ]*' | sort -u
hadolint --no-color leaky.Dockerfile | cut -d' ' -f1-2; docker rmi -f l3-leaky >/dev/null
Output
API_KEY=sk-demo-0123456789
NPM_TOKEN=npm_demo_abc123
leaky.Dockerfile:2 DL3064
leaky.Dockerfile:3 DL3064

Both values were recoverable from the history even though the .npmrc file was deleted, and Hadolint 12,433 's DL3064 ("potentially sensitive data should not be used in the ARG or ENV commands") flagged both instructions. Pass build-time credentials as secret mounts (RUN --mount=type=secret, Secret Mounts), which never reach a layer. Pass run-time credentials at run time, as Compose 514 or Kubernetes 5,150 secrets mounted as files (Secrets and Configs in Compose), not as ENV in the Dockerfile. Keep .env, keys and .npmrc out of the build context with .dockerignore (.dockerignore). Scan images for leaked credentials with trivy 67,732 image --scanners secret. And if a secret did reach a pushed image, rotate it at once: deleting the tag does not remove copies already pulled.