Refuse production startup when the Heimdall app secret is blank #83
4 changed files with 47 additions and 6 deletions
|
|
@ -60,9 +60,10 @@ New since the live release, and not read by the validator:
|
|||
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`
|
||||
Discord and Patreon sign-in. Outside Development and Testing startup fails when neither this nor
|
||||
`Heimdall__AppSecretFile` supplies a non-blank secret (the message names both options, never a value), because
|
||||
`HeimdallOptions.IsDiscordConfigured` and `IsPatreonConfigured` would otherwise be false and those sign-ins silently
|
||||
stop working (GitHub sign-in is unaffected). 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
|
||||
from logs.
|
||||
- **`Bifrost__Oidc__*`**: optional. Absent, the OIDC issuer is not mapped and discovery answers 404. Present, it must be
|
||||
|
|
@ -135,7 +136,7 @@ All must hold before step 1. A missing one means the window is not used.
|
|||
not a workstation.
|
||||
6. **Env diff done.** Every row of the Configuration table is marked present by the reader, `ConceptVotes__NetworkTagSecret`
|
||||
included and at least 32 characters (a count, not the value), and the `Heimdall__AppSecret` line is in `bifrost.env`
|
||||
(mode 600, owner `bifrost`) with Heimdall holding the same value. Nothing in "Configuration" is left unresolved.
|
||||
(mode 600, owner `bifrost`; a build with the empty-secret refusal does not start without it or `Heimdall__AppSecretFile`) with Heimdall holding the same value. Nothing in "Configuration" is left unresolved.
|
||||
7. **Quiet.** No motion closing and no known vote in flight inside the window. Check the motions page. Members told that
|
||||
everyone is signed out once (see "What moves").
|
||||
8. **Watchdog.** `bifrost-watchdog-notice.timer` fires one minute after boot and one minute after each run
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
- 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.
|
||||
- Neither set: startup fails outside Development and Testing, naming `Heimdall:AppSecretFile` and `Heimdall:AppSecret`
|
||||
(`StartupConfigurationValidator`). Development and Testing still start, with Discord and Patreon sign-in off.
|
||||
|
||||
Under systemd, pass the file with `LoadCredential=heimdall-app-secret:<root-owned file>` and
|
||||
`Environment=Heimdall__AppSecretFile=%d/heimdall-app-secret`.
|
||||
|
|
|
|||
|
|
@ -105,6 +105,13 @@ public sealed class StartupConfigurationValidator(
|
|||
failures.Add("Heimdall:PatronSupportIntakeSecret is required when patron support intake is enabled.");
|
||||
}
|
||||
|
||||
// The options are already resolved, so a secret from Heimdall:AppSecretFile or Heimdall:AppSecret lands in AppSecret.
|
||||
// Blank would silently switch Discord and Patreon member sign-in off.
|
||||
if (string.IsNullOrWhiteSpace(heimdallOptions.Value.AppSecret))
|
||||
{
|
||||
failures.Add("Heimdall:AppSecretFile (or Heimdall:AppSecret) must supply the Heimdall app secret outside development.");
|
||||
}
|
||||
|
||||
if (stripeOptions.Value.EnableCheckout)
|
||||
{
|
||||
if (!stripeOptions.Value.IsCheckoutConfigured)
|
||||
|
|
|
|||
|
|
@ -145,6 +145,36 @@ public sealed class HostTrustTests : IClassFixture<TestWebApplicationFactory>
|
|||
Assert.DoesNotContain(canary, refusal.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Production_without_a_heimdall_app_secret_refuses_startup_naming_the_options(string blank)
|
||||
{
|
||||
var refusal = Assert.Throws<InvalidOperationException>(
|
||||
() => Validate(keysPath: "/keys", appSecret: blank));
|
||||
|
||||
Assert.Contains("Heimdall:AppSecretFile", refusal.Message);
|
||||
Assert.Contains("Heimdall:AppSecret", refusal.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Production_with_a_heimdall_app_secret_passes_and_other_refusals_never_echo_it()
|
||||
{
|
||||
Validate(keysPath: "/keys", appSecret: ValidAppSecret);
|
||||
|
||||
var refusal = Assert.Throws<InvalidOperationException>(
|
||||
() => Validate(keysPath: null, appSecret: ValidAppSecret));
|
||||
Assert.DoesNotContain(ValidAppSecret, refusal.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Development")]
|
||||
[InlineData("Testing")]
|
||||
public void Development_and_Testing_start_without_a_heimdall_app_secret(string environment)
|
||||
{
|
||||
Validate(keysPath: null, environment: environment, appSecret: "");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Production_without_network_tag_secret_fails_and_never_echoes_values()
|
||||
{
|
||||
|
|
@ -315,12 +345,14 @@ public sealed class HostTrustTests : IClassFixture<TestWebApplicationFactory>
|
|||
}
|
||||
|
||||
private const string ValidTagSecret = "tag-secret-0123456789-0123456789-abcdef";
|
||||
private const string ValidAppSecret = "app-secret-5c0e91d4";
|
||||
|
||||
private static void Validate(
|
||||
string? keysPath,
|
||||
string environment = "Production",
|
||||
string? connectionString = "Host=db",
|
||||
string? tagSecret = ValidTagSecret,
|
||||
string appSecret = ValidAppSecret,
|
||||
int ipv4Length = 32,
|
||||
int ipv6Length = 56,
|
||||
string[]? allowedOrigins = null,
|
||||
|
|
@ -344,7 +376,7 @@ public sealed class HostTrustTests : IClassFixture<TestWebApplicationFactory>
|
|||
hostEnvironment,
|
||||
Options.Create(new GitHubOAuthOptions { ClientId = "id", ClientSecret = "secret" }),
|
||||
Options.Create(new GitHubAppOptions { EnableWebhookSync = false }),
|
||||
Options.Create(new HeimdallOptions { EnablePatronSupportIntake = false }),
|
||||
Options.Create(new HeimdallOptions { EnablePatronSupportIntake = false, AppSecret = appSecret }),
|
||||
Options.Create(new StripeOptions()),
|
||||
Options.Create(new BifrostHostOptions
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue