SECRET_BACKEND matrix
SECRET_BACKEND controls where SupaCloud loads application-level secrets at
boot. Source: server/src/app/secret_hydration.rs, ADR 0036 (Issue #276).
Values
Section titled “Values”| Value | Aliases accepted | Behavior |
|---|---|---|
openbao |
vault, bao |
OpenBao is the authoritative source. At boot, AppConfig::load_from_vault reads supacloud/app (KV v2) and fills any env field still empty. A required secret absent from both vault and env is fatal at boot. Env always wins over vault for any individual key. |
env |
internal |
OpenBao is never contacted. All secrets come from the process environment only. VaultManager is constructed unconfigured. |
| (unset) | — | Auto-detects: picks openbao when vault auth is present (VAULT_TOKEN non-empty, or both OPENBAO_ROLE_ID and OPENBAO_SECRET_ID non-empty), otherwise env. An unknown value is a hard boot error. |
Required vault auth (openbao mode)
Section titled “Required vault auth (openbao mode)”| Auth method | Required env vars |
|---|---|
| Static token | VAULT_TOKEN |
| AppRole | OPENBAO_ROLE_ID + OPENBAO_SECRET_ID |
| Vault address (both methods) | OPENBAO_ADDR (or VAULT_ADDR; default https://vault.blockworx.tech) |
Both methods use the same VaultManager code path. AppRole tokens are renewed
at ~token_ttl/2 by a background loop; a 403 on any KV read triggers a
re-login (server/src/integrations/vault/renewal.rs).
Vault path layout
Section titled “Vault path layout”All paths are KV v2 under the configured vault address.
| Path | Read timing | Keys stored |
|---|---|---|
supacloud/app |
Once at boot (hydrate_app_secrets) |
SUPACLOUD_JWT_SECRET, OIDC_CLIENT_SECRET, SUPACLOUD_MCP_CONFIRMATION_SECRET, DISCORD_BOT_TOKEN, SUPACLOUD_LINEAR_OAUTH_CLIENT_ID, SUPACLOUD_LINEAR_OAUTH_CLIENT_SECRET, SUPACLOUD_NOTION_OAUTH_CLIENT_ID, SUPACLOUD_NOTION_OAUTH_CLIENT_SECRET, SUPACLOUD_CREDENTIAL_ENCRYPTION_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET |
supacloud/db |
Once at boot | CON_UL (DB connection URL), DB_PASSWORD |
supacloud/messaging |
Once at boot | bot_token, bot_username, webhook_secret (Telegram) |
supacloud/ai |
On demand (cached) | AI provider API keys (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY) |
supacloud/registry |
Fresh per call | Docker registry credentials (PAT rotates independently) |
Keys in supacloud/app use their exact env-var name so vault content, SOPS
source, and documentation map 1:1. The per-subsystem paths (db, messaging,
ai, registry) are unchanged by ADR 0036 and are not duplicated in
supacloud/app.
Credential-encryption key handling
Section titled “Credential-encryption key handling”SUPACLOUD_CREDENTIAL_ENCRYPTION_KEY from supacloud/app is decoded into a
write-once in-process holder (secret_store::seed_credential_encryption_key)
— never written to std::env. This prevents the key from being inherited
by agent containers or code-runner containers (which assemble their environment
from an explicit allowlist, not the server’s full env).
The holder is write-once by design: rotating the key is a redeploy, not a live swap.
Boot validation order
Section titled “Boot validation order”AppConfig::from_env— parse-time validation (enum membership, numeric ranges) plus the prod guards that vault cannot satisfy (e.g.SUPACLOUD_INITIAL_ADMIN_EMAILSin prod, proxy-required headers). The secret-strength / vault-satisfiable prod guards (JWT length, prod encryption key, prodPUBLIC_URL) are deliberately deferred tovalidate_required.AppConfig::load_from_vault— readssupacloud/app; callshydrate_app_secretsto fill still-empty fields (env wins).AppConfig::validate_required— prod guards run after hydration, so a vault-provided secret satisfies them. Guards: JWT ≥ 32 bytes, encryption key present inMODE=prod,PUBLIC_URLset inMODE=prod.
This ordering is what prevents the bw-infra#1326 crash-loop shape (prod guard firing before vault hydration).
Lazy Class-3 secrets
Section titled “Lazy Class-3 secrets”Secrets not held on AppConfig (Stripe keys, SMTP password, edition license,
operator token) are resolved at request-time call sites via
secret_store::resolve_secret(name): non-empty process env wins, otherwise the
supacloud/app map seeded at boot. In env mode the map is never seeded, so
these secrets must come from the process environment.
See also: Secret provisioning (conceptual), Environment variables.