✦ For everyone, free.

Practical knowledge for real and everyday life

Home

14.2.2.2 Production Secret Injection

A focused guide to Production Secret Injection, connecting core concepts with practical Docker and container operations.

Production secret injection is the mechanism by which a secret value moves from wherever it is stored into a running container's memory or filesystem at the moment it is needed, and the specific technique chosen determines whether that value remains protected throughout its lifecycle or becomes exposed at some point along the way.

Injection at container start versus injection at runtime

Secrets can be injected at two distinct points: when the container starts, before the main process begins, or continuously while the container runs, with values refreshed without a restart. Start-time injection is simpler and sufficient for credentials that rarely change; runtime injection is necessary for short-lived, frequently rotated credentials where restarting a container on every rotation would be disruptive.

docker run --env-file <(vault kv get -format=json secret/my-api | jq -r '.data.data | to_entries[] | "\(.key)=\(.value)"') my-api

This example fetches secrets from a store and assembles them into an env-file-style stream at the moment docker run is invoked, injecting them once, at start time, into the container's environment.

File-based injection avoids environment exposure

Injecting a secret as a file rather than an environment variable is generally the safer choice, since environment variables are visible through docker inspect and inherited by every child process, while a file's exposure can be scoped to exactly the processes that read it:

docker run -v /run/secrets/db_password:/run/secrets/db_password:ro my-api
const dbPassword = require('fs').readFileSync('/run/secrets/db_password', 'utf8').trim();

Swarm's native secrets mechanism formalizes this pattern, mounting secrets at /run/secrets/<name> as in-memory, read-only files that never touch persistent disk storage on the host.

Init-container-style injection

In orchestrators that support a distinct initialization phase, an init container or init process can fetch secrets and write them to a shared, ephemeral volume before the main application container starts, decoupling the secret-fetching logic entirely from the application image:

services:
  secret-init:
    image: vault-init:latest
    volumes:
      - secrets:/secrets

  api:
    image: my-api:latest
    depends_on:
      secret-init:
        condition: service_completed_successfully
    volumes:
      - secrets:/run/secrets:ro

volumes:
  secrets:

This separation means the application image never needs to know which secret manager is in use, or how to authenticate to it; it only needs to know where to read the resulting files from.

Sidecar-based continuous injection

For secrets that rotate frequently, a sidecar process running alongside the application can poll the secret manager and rewrite the mounted files whenever a new version becomes available, without restarting the application container:

vault agent -config=/vault/config/agent.hcl
template {
  source      = "/vault/templates/db-password.tpl"
  destination = "/vault/secrets/db-password"
  command     = "kill -HUP 1"
}

The command directive shown here sends a signal to the application process whenever the rendered secret file changes, giving the application a chance to reload the new value without a full container restart, provided the application implements a corresponding signal handler.

Build-time secret injection

A distinct and narrower case is injecting a secret temporarily during an image build, for example to authenticate to a private package registry, where the value must never persist in any resulting image layer:

RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm install
docker build --secret id=npm_token,src=./npm_token.txt -t my-api .

BuildKit makes the secret available only to the specific RUN instruction that mounts it, and the value is never written into the image's layer history, which distinguishes this from an ARG or ENV instruction that would leave the value permanently embedded.

CI/CD pipeline injection

Secrets used during the build and deployment pipeline itself, rather than by the running application, are typically injected by the CI/CD platform's own secret store directly into the pipeline's environment for the duration of the job:

deploy:
  script:
    - echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin
    - docker push registry.example.com/my-api:1.4.0

These pipeline-scoped secrets should be masked in job logs by the CI platform and should never be echoed or written to a file that persists as a build artifact.

Verifying injection without exposing the value

After deploying, confirming that secret injection succeeded should be possible without ever revealing the secret's actual contents:

docker exec my-api sh -c '[ -s /run/secrets/db_password ] && wc -c < /run/secrets/db_password'

Checking presence and a plausible byte count is usually enough to confirm the injection mechanism worked, without needing to print or log the value itself during verification.

Common mistakes

  • Injecting a frequently rotated secret only at container start, forcing a full container restart on every rotation cycle instead of using a sidecar or template-based refresh mechanism.
  • Using ARG or ENV to pass a secret during a build instead of a BuildKit secret mount, leaving the value permanently recoverable from the image's layer history.
  • Logging the secret-fetching command's output during debugging, inadvertently writing the secret's value into a log file or terminal scrollback.
  • Mounting a secret with broader file permissions than necessary, allowing any process inside the container to read a value that only one specific process actually needs.
  • Relying on CI/CD pipeline log masking as the only protection for a secret, without also ensuring it is never written to a build artifact, cache, or intermediate file that survives the job.

Reliable production secret injection chooses the mechanism (start-time, init-phase, sidecar, or build-time) that matches how frequently a given secret changes and how sensitive its exposure window is, consistently prefers file-based delivery over environment variables, and treats verification as something that should never require revealing the value being verified.