fix(auth): emit first-run bootstrap banner to stderr (BUG-1182) (#428)

slog's text handler is contractually one-line-per-record and escapes
literal newlines as `\n`, so the multi-line bootstrap banner rendered
as a single wide line in `docker logs` — exactly the surface where
operators look for the token. Switches the banner to fmt.Fprint to
stderr (real newlines), with a companion slog.Info one-liner so
structured-log aggregators still record the event.

The companion log deliberately does NOT include the URL or token in
its structured fields — those would be parseable as
log-aggregator-extractable values, defeating the URL-fragment design
(TASK-1167 F10) that keeps the token off-server. Operators / agents
that want the token programmatically read the on-disk file at
token_path.

Verified locally: banner now renders the ASCII box with real
newlines, token visible, companion slog line shows token_path
without the URL.

Caught by Dave during the v0.3.0-rc.1 smoke test on a real Unraid box.
This commit is contained in:
xarmian
2026-05-06 10:29:38 -04:00
committed by GitHub
parent 4c62a27e3b
commit 693f03be3c
+23 -2
View File
@@ -798,8 +798,29 @@ func logBootstrapBanner(token string, cfg *config.Config, tokenPath string) {
To regenerate, delete %s and restart.
========================================================================`, url, tokenPath)
slog.Info(banner)
========================================================================
`, url, tokenPath)
// BUG-1182: bypass slog for the banner. slog's text handler is
// contractually one-line-per-record and escapes literal newlines as
// `\n`, which renders the multi-line banner as a single wide line in
// `docker logs` — exactly the surface where operators look for it.
// Banner-style operator output isn't structured logging; stderr is the
// conventional channel for it (kubectl / docker / helm / systemd all
// do this). docker logs captures stderr alongside stdout, so the
// banner stays visible.
fmt.Fprint(os.Stderr, banner)
// Companion structured log so log aggregators that parse slog JSON
// still record the event. Deliberately does NOT include the URL or
// token — those are in the stderr banner where the operator looks
// for them. Repeating the URL as a parseable structured field would
// give log aggregators an easy-to-extract token, which is precisely
// what the URL-fragment design (TASK-1167 F10) is trying to avoid.
// Operators / agents that want the token programmatically should
// read the token_path file directly.
slog.Info("first-run bootstrap setup banner emitted to stderr — see container logs",
"token_path", tokenPath)
}
// --- stop ---