Secrets
Secrets are not a standalone resource. A service declares the runtime values it needs in its
environment.yaml sidecar — an ENV_VAR_NAME: <source> map — and inforge injects each as an
environment variable when the service starts. This page covers the source DSL, the git-encrypted store
behind vault:, and how a resolved value reaches a running service. Secret values are never baked
into an artifact.
DATABASE_URL: ref:database/main.connectionUrl # an output from another resource
CF_TOKEN: env:CLOUDFLARE_API_TOKEN # a deploy-environment variable
API_KEY: vault:API_KEY # a value from the git-encrypted store
LOG_LEVEL: info # a literal (non-secret config) value
Environment variables are container-scoped: every service sharing a container receives the same
set of values. The inforge secret CLI keys the encrypted store by (container, KEY)
and takes a service name only as a handle onto its container.
Source DSL
Each entry in environment.yaml is a source DSL string naming where the value comes from — never
the value itself. There are four kinds:
ref:<database|compute>/<name>.<output>
A runtime output of another resource:
DATABASE_URL: ref:database/main.connectionUrl
SERVER_IP: ref:compute/bridge.publicIp
The parts are the resource type (database or compute), the resource name, and the
output field. A global/ prefix on the name targets the global slice:
DATABASE_URL: ref:database/global/shared.connectionUrl
EDGE_IP: ref:compute/global/edge-01.publicIp
This is the one allowed cross-region reference: a regional service secret may read a global database or compute output, resolving against the global slice regardless of the service's region. See Global resources for the full rules.
env:<VAR>
A value read from the deploy process environment — the same mechanism variables.yaml /
regions.yaml use for provider credentials:
CF_TOKEN: env:CLOUDFLARE_API_TOKEN
You inject CLOUDFLARE_API_TOKEN however you like — typically a CI secret mapped to an env var in your
workflow (env: { CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} }). An unset or empty
value fails the deploy loudly rather than materialising an empty secret. env: is correct for values
genuinely external to the deploy; for app secrets that should live in git, prefer vault:.
vault:<KEY>
A value held age-encrypted in git, in the environment's committed secret store
(resources/<env>/secrets.enc.yaml), keyed by the service's container and <KEY>:
API_KEY: vault:API_KEY # store key == env-var name
DATABASE_URL: vault:PROD_DB_URL # store key decoupled from the env-var name
The store key is independent of the env-var name, so DATABASE_URL: vault:PROD_DB_URL is valid. Values
are written with the inforge secret CLI — the only writer of the store — and encrypted
to the store's committed public recipient, so anyone with commit access can add or replace a
secret value without any private key (and cannot decrypt what they wrote). At deploy, inforge decrypts
the values in CI with the master identity from the INFORGE_SECRETS_KEY environment variable and writes
the plaintext into the provider, exactly where the other source kinds land.
vault: is the right default for app secrets that should live in git (API keys, signing keys, tokens).
The provider is a derived cache: it is written only by the deploy, never by the CLI, so it always
reflects the last deployed git state (see
ADR-0017).
inforge validate fails if a vault: secret has no matching ciphertext entry in the env's store, so a
missing value is caught before any deploy.
See Creating a secret below for the full workflow.
literal
Any string without a ref:/env:/vault: prefix is used verbatim — useful for non-secret
per-service configuration delivered through the same env-var mechanism:
LOG_LEVEL: info
API_BASE: https://api.example.com/v1
:::warning Not for real secrets
A literal value is committed in plaintext (it lives in git). Use it for non-secret configuration
only; use vault:, env: or ref: for anything sensitive.
:::
Creating a secret
A vault: value lives in the env's committed, age-encrypted store. The full lifecycle:
-
Initialise the store once per environment (see Initialising the store):
inforge secret init prd -
Declare the secret on the consuming service in its
environment.yaml:API_KEY: vault:API_KEY -
Write the value with the
inforge secretCLI (the<service>argument resolves to its container; the value is read from stdin, or--generatemints a random one):pbpaste | inforge secret set prd api API_KEY # value from stdininforge secret set prd api SESSION_KEY --generate # random 32-byte value -
Commit and merge the updated
secrets.enc.yaml. The provider is written byinforge deployon merge — never by the CLI — so a deploy frommaincan never roll back a value the provider already serves. -
Restart the consumers after the deploy lands, so they re-fetch at start:
inforge service restart prd api
Adding the 2nd…Nth secret never touches your workflow — only step 1 (the one-time
INFORGE_SECRETS_KEY) does.
Initialising the store
A repository uses the encrypted store once you run inforge secret init <env> for an environment. It
creates resources/<env>/secrets.enc.yaml, generates a fresh age key pair, writes the public
recipient into the store, and prints the master identity (AGE-SECRET-KEY-…) once:
inforge secret init prd
- Save the printed identity as the
INFORGE_SECRETS_KEYGitHub Actions secret in the consumer repo — the deploy decrypts with it (see the deploy starter). - Keep an out-of-band backup: losing it means re-setting every secret in the environment.
- Commit the generated
secrets.enc.yaml. Anyone with commit access can add values against the public recipient with no key material; reading a value requires the master identity, which is never committed.
Full subcommand reference (set, ls, rm, rotate) and incident-response runbooks are on the
inforge secret page.
How secrets reach a service
inforge does not bake secret values into any artifact. At deploy time, for each service that declares secrets, inforge:
- Resolves every
environment.yamlentry (ref:from stack outputs,vault:by decrypting the store,env:from the deploy environment, literals verbatim) and writes the values to the secrets provider under the service's scoped path. - Mints a per-service machine identity, scoped read-only to that service's path.
- Writes two files onto the host under
/etc/wardnet/services/<service>/:descriptor.yaml— a secret-free document with the provider coordinates and an env-var → vault-key mapping.credential.age— the machine-identity credential, age-encrypted to the host's own SSH host key (encrypted in memory to the key read over SSH; the plaintext never lands on disk).
At service start, inforge-bootstrap (the systemd ExecStart for every service) decrypts the
credential with the host key, logs in to the provider, fetches the secrets, injects them as environment
variables, drops privilege to the service's user, and execs the real binary. Secret values live
only in the service process's environment — never on disk, in the journal, or in argv. A service that
declares no secrets gets a descriptor with no provider and starts with no fetch at all.
The secrets provider a service's values are written to is selected per region by the infisical block
in that region's providers: in regions.yaml — there is no
secretsStore key in inforge.yaml. A service that declares environment variables in a region (or
global slice) with no matching secrets provider fails inforge validate, before any deploy.