diff --git a/docs/concept-mill-votes-target.md b/docs/concept-mill-votes-target.md index 30a523e..40935af 100644 --- a/docs/concept-mill-votes-target.md +++ b/docs/concept-mill-votes-target.md @@ -41,7 +41,7 @@ Bifrost head `bf83a93` (2026-10-10; `20d5efa` plus the prior-art note, docs only ### Data Protection on the live host -18. The live env file sets no `DataProtection__KeysPath` (key names only: `AllowedHosts`, `Bootstrap__*`, `ConnectionStrings__Bifrost`, `GitHubApp__EnableWebhookSync`, `GitHubOAuth__*`, `Heimdall__*`, `Host__*`, `Stripe__*`). So `Program.cs:113-119` never calls `PersistKeysToFileSystem`, and ASP.NET Core's default applies: the service runs as `bifrost` with `HOME=/srv/bifrost`, and the key ring is `/srv/bifrost/.aspnet/DataProtection-Keys` with two keys (created 2026-04-22 and 2026-07-22, the 90-day roll; prior-art note `docs/research/anonymous-vote-stuffing-prior-art.md:65-69`). A third, unused directory `/srv/bifrost/data-protection-keys` holds one key from 2026-07-15. Keys therefore persist across restarts today by accident of `HOME`, not by configuration. +18. The live env file sets no `DataProtection__KeysPath` (key names only: `AllowedHosts`, `Bootstrap__*`, `ConnectionStrings__Bifrost`, `GitHubApp__EnableWebhookSync`, `GitHubOAuth__*`, `Heimdall__*` (the app secret as `Heimdall__AppSecret`; `Heimdall__AppSecretFile` is the replacement), `Host__*`, `Stripe__*`). So `Program.cs:113-119` never calls `PersistKeysToFileSystem`, and ASP.NET Core's default applies: the service runs as `bifrost` with `HOME=/srv/bifrost`, and the key ring is `/srv/bifrost/.aspnet/DataProtection-Keys` with two keys (created 2026-04-22 and 2026-07-22, the 90-day roll; prior-art note `docs/research/anonymous-vote-stuffing-prior-art.md:65-69`). A third, unused directory `/srv/bifrost/data-protection-keys` holds one key from 2026-07-15. Keys therefore persist across restarts today by accident of `HOME`, not by configuration. 19. The service's working directory resolves to the release directory: `readlink /proc//cwd` = `/srv/bifrost/app/releases/20260723-daedcc4`. Data Protection's default application discriminator is the content-root path (prior-art note `:67`), so every deploy into a new release directory isolates payloads protected by the previous release: auth cookies, antiforgery tokens and any future device token stop decrypting after each deploy. Mechanism from source docs plus this probe; not observed as logged-out users. `SetApplicationName` is not called anywhere (`git grep SetApplicationName bf83a93` is empty). ### Proxy and client address diff --git a/docs/deploy-main-to-live.md b/docs/deploy-main-to-live.md index 88997ec..c0f0e14 100644 --- a/docs/deploy-main-to-live.md +++ b/docs/deploy-main-to-live.md @@ -54,7 +54,13 @@ permits, windows and `MaxTrackedKeys`. New since the live release, and not read by the validator: -- **`Heimdall__AppSecret`**: required for Discord and Patreon sign-in. Without it `HeimdallOptions.IsDiscordConfigured` and +- **`Heimdall__AppSecretFile`**: the preferred way to give Bifrost the app secret below. A path to a file holding it (one + trailing newline is trimmed), for example `%d/heimdall-app-secret` under systemd `LoadCredential=`, so the secret never + rides in the process environment. When set, the file wins over `Heimdall__AppSecret`; a missing, unreadable or empty file + stops startup in every environment, with a message that names the option and path and never the value. Ops steps: + `docs/heimdall-app-secret-file.md`. +- **`Heimdall__AppSecret`** (deprecated; use `Heimdall__AppSecretFile`; startup logs a warning when it is set): required for + Discord and Patreon sign-in. Without it `HeimdallOptions.IsDiscordConfigured` and `IsPatreonConfigured` are false and those sign-ins stop working (GitHub sign-in is unaffected); startup does not fail, so a missing key shows only at the live check. The value is Heimdall's app secret for slug `bifrost` (`GC_ACCESS_APP_BIFROST_SHARED_SECRET` on the Heimdall side). Bifrost sends it as `x-heimdall-app-secret`, redacted diff --git a/docs/heimdall-app-secret-file.md b/docs/heimdall-app-secret-file.md new file mode 100644 index 0000000..063dd7d --- /dev/null +++ b/docs/heimdall-app-secret-file.md @@ -0,0 +1,17 @@ +# Heimdall app secret from a file + +`Heimdall:AppSecret` (`Heimdall__AppSecret`) is Bifrost's app secret for Heimdall slug `bifrost`; it is sent as the +`x-heimdall-app-secret` header. Set through the environment it rides in the process environment. `Heimdall:AppSecretFile` +(`Heimdall__AppSecretFile`) names a file holding it instead. + +- File set: Bifrost reads it once at startup and one trailing newline is trimmed. The file wins over `Heimdall__AppSecret`. +- File set but missing, unreadable, a directory, or empty: startup fails in every environment. The message names + `Heimdall:AppSecretFile` and the path, never the secret. +- Only `Heimdall__AppSecret` set: still works; startup logs a deprecation warning (without the value). +- Neither set: Discord and Patreon sign-in stay off, as before. + +Under systemd, pass the file with `LoadCredential=heimdall-app-secret:` and +`Environment=Heimdall__AppSecretFile=%d/heimdall-app-secret`. + +Code: `HeimdallOptions.ResolveAppSecret` (`src/Bifrost.Web/Configuration/HeimdallOptions.cs`), wired in `Program.cs`, forced at +startup by `StartupConfigurationValidator.Validate`. Tests: `tests/Bifrost.Web.Tests/HeimdallAppSecretFileTests.cs`.