Secrets and Configuration Patterns, Runtime Injection, Environment Risk, Build Secrets, and External Secret Stores: Concepts, Architecture, and Mental Model
Separate configuration from secrets and trace secret data from authorized source through build-time or runtime injection without persisting it in image history, broad environment scope, or logs.
Learning objectives
- Separate ordinary configuration, build-time secrets, runtime secrets, and bootstrap identities by lifecycle and exposure.
- Trace a credential from owner/provider through BuildKit or runtime injection to the consuming process without assuming that “inside a container” means “secret”.
-
Explain why Dockerfile
ARG/ENV, copied files, command-line arguments, and debug logs are unsafe default secret channels. - Inspect image metadata, container configuration, mounts, logs, and provider evidence without printing a credential.
1. The practical problem: configuration wants visibility; secrets do not
Configuration and secrets both change application behavior, but they have opposite operational needs. A port number, feature flag, or log level is useful when operators can inspect it. A password, private key, signing token, or registry credential should have a named owner, narrow scope, controlled delivery path, rotation policy, and revocation evidence. Treating both as “environment variables” collapses two different trust problems into one broad mechanism.
Earlier chapters established immutable image identity, build contexts, BuildKit, Compose, mounts, least privilege, and user namespaces. Those controls become meaningful here because secret safety depends on where the bytes exist, which process can read them, and which artifact could retain them.
3. State inventory before touching a credential
| State | Questions | Safe evidence |
|---|---|---|
| Owner / scope | Who issues it? What action/repository/database can it access? | Policy name, fake lab owner, provider audit ID; never the value |
| Phase | Build or runtime? | Build command/secret ID versus service/mount path |
| Image | Did metadata or a layer retain it? |
docker image inspect,
docker history --no-trunc, controlled
filesystem search for the fake value
|
| Container | Is it broad environment state or a narrow file/provider read? |
docker inspect configuration and mounts; do not
dump real values
|
| Logs | Could app/build/daemon output disclose it? | Presence/absence tests using synthetic markers only |
| Rotation | Which version is active, and how is old access revoked? | Version ID, creation timestamp, reconcile event, provider revocation record |
4. Why ARG, ENV, copied files, and command lines are different risks
ENV is image configuration and remains visible to later
containers. A Dockerfile ARG is a build input, not a
secret primitive; Docker’s build checks explicitly flag secret-like
ARG/ENV usage. Copying a private file into the context creates a
second problem: the build now has a normal source path from which
layers, cache, debugging tools, or accidental COPY instructions can
retain it.
Command-line arguments and logs are also dangerous secret surfaces because process inspection, shell history, CI transcript capture, and monitoring systems can copy them beyond Docker. The safer pattern is to pass a reference or narrow file descriptor/mount while keeping the value out of broad metadata.
5. BuildKit secret and SSH mounts
A BuildKit secret is supplied by the client and mounted only for the
relevant build instruction, normally under
/run/secrets/<id>. An SSH mount exposes an agent
socket or key material for operations such as cloning a private
repository. The build command can require the mount so that a
missing credential fails clearly rather than silently using a
public/anonymous path.
# syntax=docker/dockerfile:1
FROM alpine:3.22.1
RUN --mount=type=secret,id=repo_token,required=true test -s /run/secrets/repo_token && echo "authenticated build step completed" > /build-status
The output file is non-secret evidence that the step ran. Do not write the secret itself, a reusable transformed token, or an authorization header into the image.
6. Runtime file injection and provider boundaries
Compose can grant a secret to selected services and expose it as a
file such as /run/secrets/db_password. On Linux, local
Compose implements this with a single-file bind mount. That improves
per-service scope and avoids broad environment exposure, but it is
not an encrypted external secret store and it is not the same
mechanism as Swarm secret management.
An external manager introduces another boundary: the workload authenticates using some bootstrap identity, fetches only the secret it is authorized to read, and may cache it for a defined time. The secret manager does not remove the need to prevent logging, control process privileges, protect memory, rotate credentials, and handle provider outages.
7. Read-only baseline inspection
docker version
docker context show
docker info --format 'Security={{json .SecurityOptions}} Root={{.DockerRootDir}}'
docker buildx version
docker buildx inspect --bootstrap
docker compose version
# Safe metadata checks against an existing non-sensitive image/container
# Do not run env-dumping commands against production workloads containing secrets.
docker image inspect alpine:3.22.1 --format 'ImageEnv={{json .Config.Env}}' 2>/dev/null || true
The evidence goal is to identify the component and exposure path, not to render credential values onto your terminal.
8. DevOps contract: a secret has a lifecycle, not just a value
A reproducible secret design records owner, purpose, scope, source/provider, build-versus-runtime phase, consumer, delivery path, rotation version, revocation/cleanup procedure, and known residual exposures. The image digest proves the application artifact; it does not prove that today’s external secret value is correct or still authorized.
Knowledge check
Why is an ordinary environment variable a poor default channel for a runtime secret?
Environment variables are broad process state: they can be exposed through container configuration/inspection, inherited by child processes, included in diagnostics, or accidentally printed. A narrowly mounted file or provider API can reduce that exposure surface.
What is the main distinction between a build secret and a runtime secret?
A build secret exists only while producing the image and should not persist into the image. A runtime secret is needed by a running workload and must be delivered by a runtime mechanism with its own authorization, rotation, and cleanup lifecycle.
Does a secret mount become an image layer?
No. BuildKit exposes it ephemerally to the relevant RUN instruction. The secret value is not copied into the resulting layer unless the build command itself writes or transforms it into persistent output.
Why must “not present in docker history” not be your only leak test?
Secrets can leak through files, image config, application logs, build output, cache/export artifacts, source control, or external systems. Absence from one surface is only one piece of evidence.
What does least exposure mean here?
Only the component, phase, service, path, and time window that actually require the credential should receive it; everything else should receive ordinary non-secret configuration.
Official references and version notes
- Docker Build secrets — secret mounts, SSH mounts, Git authentication secrets, file/environment sources, and build-time scope.
-
Dockerfile reference
—
RUN --mount=type=secret,RUN --mount=type=ssh,required, target, ownership, and environment exposure options. -
SecretsUsedInArgOrEnv build check
— why Dockerfile
ARG/ENVare not secret channels. - Manage secrets securely in Docker Compose — per-service grants and file-based runtime delivery.
- Compose secrets reference — top-level secret definitions and service consumption.
- Compose environment-variable best practices — configuration precedence and why sensitive data should use secrets.
- Swarm secrets — orchestrator-managed secret semantics, intentionally distinct from local Compose file mounts.
- Docker Engine 29 release notes — current Engine baseline.
- Buildx releases and BuildKit releases — current builder/frontend assumptions.
Docker Engine 29.8.1 is the current Engine release; Buildx 0.37.1 and BuildKit 0.33.0 are the current upstream baselines used for feature notes; Compose v5.5.1 is the current Compose release. Docker documents build arguments and Dockerfile environment variables as inappropriate for secrets because sensitive values can persist in image metadata/history, while BuildKit secret/SSH mounts expose credentials only to the build step. Compose secrets are granted per service and delivered as files (on Linux, local Compose uses a single-file bind mount); that is not the same storage/trust model as Swarm secrets or an external secret manager. Labs therefore record the actually installed Engine, Buildx, BuildKit, Compose, context, platform, and secret-delivery path before drawing conclusions.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.