CoursesAdvanced container securityRuntime & network hardening

Runtime secrets, done right

Get secrets into a read-only container without env leaks.

Advanced14 min · lesson 19 of 25

You have a non-root, read-only, distroless container — now it needs a database password. How you deliver it decides whether all that hardening holds. The wrong way is the common way: an environment variable. Env vars leak through docker inspect, the process environment (readable at /proc/<pid>/environ), child processes, crash dumps, and logs — so a secret passed with -e is exposed to anyone who can look at the container, hardened or not.

terminal
# why -e is unsafe: the secret is visible several ways at once
$ docker run -d --name app -e DB_PASSWORD=S3cr3t myapp:1.0
$ docker inspect app --format '{{json .Config.Env}}'
["DB_PASSWORD=S3cr3t", ...] # right there in inspect
$ sudo cat /proc/$(docker inspect -f {{.State.Pid}} app)/environ | tr '\0' '\n' | grep DB_
DB_PASSWORD=S3cr3t # and in the process environment

Deliver secrets as files, in memory

The right pattern is a mounted file, ideally on tmpfs so it never touches disk. The app reads the file at startup; the value never enters the environment or inspect output, and a memory-backed mount vanishes when the container stops. Most official images support this directly via _FILE-suffixed variables (DB_PASSWORD_FILE) that read the value from a path. Docker secrets and the Swarm/Compose secrets mechanism implement exactly this — a read-only tmpfs file under /run/secrets.

compose.yaml
services:
app:
image: myapp:1.0
read_only: true
environment:
DB_PASSWORD_FILE: /run/secrets/db_pw # app reads the FILE, not an env value
secrets: [db_pw] # mounted at /run/secrets/db_pw (tmpfs, ro)
secrets:
db_pw:
file: ./secrets/db_pw.txt # or external: true (from docker secret)
Secret delivery, worst to best
avoid
ENV / -e
leaks via inspect, /proc, logs
baked into image
in a layer forever, pullable
ARG at build
lands in build history
prefer
tmpfs file / Docker secret
in memory, ro, not in inspect
external store (Vault)
short-lived, rotated, audited
The rule: secrets are files, not environment variables — and the best secret is one the app fetches, uses, and never stores.

External stores for rotation and audit

A mounted file is a static secret; the stronger pattern keeps the source of truth outside the container entirely. A secrets manager — HashiCorp Vault is the common one — issues short-lived, automatically-rotated credentials, and a sidecar or init (Vault Agent) fetches them and writes them to a shared tmpfs the app reads. Now a leaked credential expires on its own, rotation happens in one place, and every access is audited. The container never holds a long-lived secret.

terminal
# a short-lived, auto-rotated DB credential from Vault, written to a tmpfs the app reads
$ vault read database/creds/app-role
username v-app-x7f2... # unique, expires in 1h
password A1b2C3... # rotated automatically; revoked on lease end
# the Vault Agent sidecar renders this into /run/secrets/db, shared via tmpfs
Never bake secrets into images or ARGs either
Two adjacent mistakes: a secret COPY-ed into the image lives in a layer forever and ships to everyone who can pull it, and a build-time ARG lands in the image history. Neither is fixed by deleting the file later. Keep secrets out of layers entirely — mount them at runtime as tmpfs files (BuildKit --mount=type=secret for build-time), and prefer an external store so they are short-lived and rotated.