AppSecretFile: pin the deprecation at Warning level, say trailing newlines

Soul pass on 25b9cc16: the warning level was unpinned (a Trace demotion passed) and the docs said one newline where the code trims all.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Heimdall 2026-11-12 13:46:24 +00:00
commit 280d843aeb
4 changed files with 24 additions and 6 deletions

View file

@ -55,7 +55,7 @@ permits, windows and `MaxTrackedKeys`.
New since the live release, and not read by the validator:
- **`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
trailing newlines are 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`.

View file

@ -4,7 +4,7 @@
`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: Bifrost reads it once at startup and trailing newlines are 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).

View file

@ -45,7 +45,7 @@ public sealed class HeimdallAppSecretFileTests : IDisposable
[InlineData("")]
[InlineData("\n")]
[InlineData("\r\n")]
public void The_file_is_the_secret_with_only_the_trailing_newline_trimmed(string ending)
public void The_file_is_the_secret_with_only_trailing_newlines_trimmed(string ending)
{
var options = Resolve(appSecretFile: Write(FileSecret + ending));
@ -67,7 +67,7 @@ public sealed class HeimdallAppSecretFileTests : IDisposable
var options = Resolve(EnvSecret, Write(FileSecret + "\n"));
Assert.Equal(FileSecret, options.AppSecret);
Assert.Contains(_logs.Lines, line => line.Contains("deprecated"));
Assert.Contains(_logs.WarningLines, line => line.Contains("deprecated"));
AssertNoSecretLogged();
}
@ -110,7 +110,7 @@ public sealed class HeimdallAppSecretFileTests : IDisposable
var options = Resolve(EnvSecret);
Assert.Equal(EnvSecret, options.AppSecret);
Assert.Single(_logs.Lines, line => line.Contains("deprecated") && line.Contains("Heimdall:AppSecretFile"));
Assert.Single(_logs.WarningLines, line => line.Contains("deprecated") && line.Contains("Heimdall:AppSecretFile"));
AssertNoSecretLogged();
}

View file

@ -5,6 +5,19 @@ namespace Bifrost.Web.Tests.Support;
public sealed class CapturingLoggerProvider : ILoggerProvider
{
private readonly List<string> _lines = [];
private readonly List<string> _warnings = [];
/// <summary>Lines logged at Warning or above, to pin that an operator-facing notice reaches the journal.</summary>
public IReadOnlyList<string> WarningLines
{
get
{
lock (_lines)
{
return _warnings.ToArray();
}
}
}
public IReadOnlyList<string> Lines
{
@ -38,7 +51,12 @@ public sealed class CapturingLoggerProvider : ILoggerProvider
{
lock (owner._lines)
{
owner._lines.Add(formatter(state, exception) + (exception is null ? string.Empty : "\n" + exception));
var line = formatter(state, exception) + (exception is null ? string.Empty : "\n" + exception);
owner._lines.Add(line);
if (logLevel >= LogLevel.Warning)
{
owner._warnings.Add(line);
}
}
}
}