Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Manage secrets

Secrets are an opaque Secret type: shown as [REDACTED] in every log, plan, apply output, error, and the state cache — only their origin (${env:NAME}) is ever displayed. A literal secret in YAML is rejected at parse time. The defences layer up:

LayerWhat
L0 Opaque Secret typeredaction by construction (the value has no visible accessor)
L1 Env interpolation${env:VAR} in Param fields and value_secret
L2 SOPS + age at rest${sops:path} decrypted in memory from a colocated secrets.enc.yaml
L4 validate --strictflags a secret-like value mistakenly put in a visible value
L5 Redacted logs/planguaranteed by L0; no value can reach output
L6 Audit logappend-only 0600 log of who/what, with secret origins and a diff hash, never values

Ways to supply a value:

env_vars:
  - name: NODE_ENV
    value: "production"                     # visible literal
  - name: LOG_LEVEL
    value: "${env:LOG_LEVEL}"               # visible, env-resolved
  - name: DATABASE_URL
    value_secret: "${env:DATABASE_URL}"     # secret, env-sourced, REDACTED
  - name: STRIPE_KEY
    value_secret: "${sops:stripe.key}"      # secret, SOPS-decrypted, REDACTED

When ${env:…} references resolve

Loading is lenient: validate, plan and explore keep every ${env:…} reference (visible values and value_secret alike) unresolved — a secret carries only its origin — so these read-only commands work even when the secret environment is not set. The values are bound at exactly one place: apply, which resolves them just before pushing. If a referenced variable is unset, apply fails with a clear, resource-named error rather than pushing an empty value; destroy never resolves them. ${sops:…} references are the exception — they are decrypted at load, since that needs the manifest directory.

Because of this, validate (including --strict) checks structure and schema only and no longer reports an unset secret environment variable; run apply to surface a missing one.

SOPS + age secrets at rest

A ${sops:path} reference is read from a secrets.enc.yaml colocated with the manifest (the path is never user-supplied, so there is no traversal surface), decrypted in memory, and shown as [REDACTED]. path is a dotted key into the decrypted document, e.g. databases.staging.password. A worked example lives in examples/secrets-sops/ — its committed secrets.enc.yaml is encrypted to a throwaway recipient, so re-encrypt it with your own key before running it.

# 1. Generate an age key and restrict it (iac-coolify refuses a group/other-readable key).
age-keygen -o ~/.config/sops/age/keys.txt && chmod 600 ~/.config/sops/age/keys.txt

# 2. Put your public key (age1...) in your .sops.yaml, then encrypt:
printf 'databases:\n  staging:\n    password: your-secret\n' \
  | sops --encrypt --input-type yaml /dev/stdin > secrets.enc.yaml

# 3. Validate — the SOPS value resolves and stays REDACTED.
iac-coolify validate coolify.yaml

iac-coolify locates the key via SOPS_AGE_KEY_FILE (or the default ~/.config/sops/age/keys.txt). Never commit a private age key — only the encrypted file and the public recipient (.sops.yaml) are safe to track. Add a pattern such as *.age.key to your .gitignore so a key exported next to the manifests can never be committed.

Viewing secret values

iac-coolify never reveals secret values, by design. To inspect one, go to the source — e.g. printenv DATABASE_URL. Secrets stay scoped to the tool that owns them (your shell).