feat(secrets): file-based production secrets via compose secrets (BUNYIP-38) #40
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/bunyip-38-file-based-secrets"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes BUNYIP-38. Raised in the BUNYIP-32 (PR #33) review: every production secret was a plain compose environment variable, visible to anyone with docker socket access (
docker inspect bunyip-api) and to any process able to read /proc//environ.What
Mechanism: docker compose file-based
secrets:(the same family as the existing OIDC key mount). Each secret is one value in one file under./secrets/(gitignored), mounted read-only at/run/secrets/<name>.Config layer:
secret_env(name)helper (crates/bunyip-domain/src/config.rs). Resolution order:{NAME}_FILE(trimmed file contents; unreadable file panics at startup) then plain{NAME}env var. Empty values are treated as unset, so compose${VAR:-}defaults and empty secret files both mean "not configured". Applied to every secret read:DATABASE_URLConfig::from_env(embeds the postgres password)JWT_SECRETmain.rsTOTP_ENCRYPTION_KEY(+_PREV)Config::load_totp_encryption_keySTRIPE_ENCRYPTION_KEY(+_PREV)Config::load_stripe_encryption_keySETUP_DEFAULT_ADMINmain.rsFORGEJO_API_TOKENDownloadConfig::from_envcompose.yml: top-level
secrets:block (7 files), api consumes them via*_FILEenv vars, postgres uses its nativePOSTGRES_PASSWORD_FILE. No secret value remains in anyenvironment:section. Quick-start header documents the file-creation commands; all files must exist (empty = not configured).Docs:
.env.example(secrets section note + production subsection listing the 7 files; the secret vars are now dev-only) anddev-docs/oci-registry-verification.md(production notes: token lives in./secrets/forgejo_api_token).Dev workflow unchanged: compose.dev.yml untouched; the env-var fallback path keeps
just dev/just dev-ssoworking with plain.envvalues.Behavioral edge cases (intentional improvements)
TOTP_ENCRYPTION_KEY/STRIPE_ENCRYPTION_KEYin dev now falls back to the zero dev key (matching the documented intent) instead of panicking with a confusing "must be exactly 32 bytes" error.SETUP_DEFAULT_ADMINnow skips admin seeding instead of panicking on first boot ("must be in format email:password").JWT_SECRETin production now fails fast at startup instead of silently signing tokens with an empty/placeholder secret.Acceptance criteria
docker inspect bunyip-apishows no secret values: verified viadocker compose -f compose.yml config; the rendered api environment contains only/run/secrets/*paths.Verification (rust-builder-glibc 1.94.1 container)
cargo clippy --workspace --all-targets -- -D warnings: cleancargo fmt --all --check: cleancargo test --workspace --lib: 206 passed, 0 failed (5 newsecret_envtests)docker compose -f compose.yml config: valid; rendered environment has no secret values- Add secret_env() to the config layer: reads {NAME}_FILE (trimmed contents of a compose secret mounted under /run/secrets, panicking on an unreadable file) with fallback to the plain {NAME} env var; empty values count as unset so ${VAR:-} compose defaults and empty secret files both mean "not configured". - Apply it to every secret: DATABASE_URL, JWT_SECRET, TOTP/STRIPE_ENCRYPTION_KEY (+_PREV rotation keys), SETUP_DEFAULT_ADMIN, FORGEJO_API_TOKEN. Dev (.env via just dev / dev-sso) is unchanged through the fallback path. - compose.yml: top-level secrets: block with 7 file-based secrets under ./secrets/ (gitignored); the api consumes them via *_FILE env vars, postgres via its native POSTGRES_PASSWORD_FILE. No secret value remains in any environment: section, so docker inspect and /proc/<pid>/environ never expose them. - Intentional edge-case improvements: empty TOTP/STRIPE keys in dev now use the documented zero dev key instead of panicking; empty SETUP_DEFAULT_ADMIN skips seeding instead of panicking; empty JWT_SECRET in production fails fast instead of silently signing with a placeholder. - Docs: .env.example (production secrets are file-based, the secret vars are dev-only) and dev-docs/oci-registry-verification.md production notes (token lives in ./secrets/forgejo_api_token). Verified in the rust-builder 1.94.1 container (clippy -D warnings clean, fmt clean, 206 lib tests pass incl. 5 new secret_env tests), compose config renders no secret values, and the live dev stack rebuilds + starts clean with the env-var fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: nrupard <natrsmith11@gmail.com>