Skip to content
Select themeSelect language

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).

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.
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).

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.

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.

  1. AppConfig::from_env — parse-time validation (enum membership, numeric ranges) plus the prod guards that vault cannot satisfy (e.g. SUPACLOUD_INITIAL_ADMIN_EMAILS in prod, proxy-required headers). The secret-strength / vault-satisfiable prod guards (JWT length, prod encryption key, prod PUBLIC_URL) are deliberately deferred to validate_required.
  2. AppConfig::load_from_vault — reads supacloud/app; calls hydrate_app_secrets to fill still-empty fields (env wins).
  3. AppConfig::validate_required — prod guards run after hydration, so a vault-provided secret satisfies them. Guards: JWT ≥ 32 bytes, encryption key present in MODE=prod, PUBLIC_URL set in MODE=prod.

This ordering is what prevents the bw-infra#1326 crash-loop shape (prod guard firing before vault hydration).

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.