From 693f03be3c02ba90cf24e098fda508129c59fab9 Mon Sep 17 00:00:00 2001 From: xarmian Date: Wed, 6 May 2026 10:29:38 -0400 Subject: [PATCH] fix(auth): emit first-run bootstrap banner to stderr (BUG-1182) (#428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cmd/pad/main.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/cmd/pad/main.go b/cmd/pad/main.go index 30f86f96..094d57c3 100644 --- a/cmd/pad/main.go +++ b/cmd/pad/main.go @@ -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 ---