feat(observe): redact secrets in the structured logger by default#62
Merged
Conversation
Session End Protocol for the CSRF hardening iteration (PR #60, ADR-006). - Archive the iteration at docs/iterations/2026-05-14-csrf-hardening.md — constant-time comparison, mandatory EncryptionKey, NewCSRFMiddleware, defensive crypto fixes, the review-loop outcome, and three deferred follow-ups. - Reset CURRENT_ITERATION.md to an empty slate; secrets redaction in slog is the top-ranked next candidate. - Refresh HANDOFF.md: main @ 643aee7, no active iteration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the secrets-in-logs gap from the 2026-05-14 audit §7 item 6. See
ADR-007.
observe.NewLogger previously built a slog.Handler with no ReplaceAttr —
any code that logged a secret-bearing attribute (authorization,
password, token, a session cookie, …) emitted it verbatim. NewLogger
now redacts: the value of any attribute whose key is in a curated,
case-insensitive denylist is replaced with RedactionPlaceholder
("[REDACTED]"). The key and log-line shape are unchanged. Pure stdlib
(slog.HandlerOptions.ReplaceAttr); no new dependency.
API (additive, contract baseline updated):
- NewLoggerWithRedaction(level, format string, RedactionConfig) — the
explicit-control constructor. RedactionConfig{Disabled, ExtraKeys,
Placeholder}.
- DefaultRedactedKeys() — exposes the built-in denylist for auditing.
- RedactionPlaceholder — the default masked value.
- NewLogger keeps its signature and delegates to NewLoggerWithRedaction
with a zero-value config (redaction on).
- log_redact_extra_keys config key (transitional) threads ExtraKeys
through App.New.
There is deliberately NO config key to disable redaction — turning it
off requires an explicit code-level opt-out via NewLoggerWithRedaction,
so the decision surfaces in code review (the ADR-004 / WithOpenAuthz
discipline).
BREAKING (pre-v1.0, stable surface): a deployment that intentionally
logged a field under a denylisted key now sees [REDACTED] there.
Documented in CHANGELOG under Changed; contract-guardian confirmed no
DEP entry is needed (no symbol removed/renamed) — same governance trail
as ADR-006.
Review-loop fixes applied in this PR:
- security MED: the auto-generated admin bootstrap password was logged
under the key "password" — now [REDACTED], which would lock the
operator out. The password is now written once to stderr, deliberately
bypassing the logger; the structured log records only that it
happened. (pkg/app/app.go)
- security MED: expanded the denylist with framework-relevant keys —
DSN/connection strings (database_url, dsn, redis_url, …), smtp_pass,
aws_secret_access_key, aws_session_token, private-key-material names,
provider tokens (oauth_token, github_token, …).
- code-review: ExtraKeys can no longer silence slog's built-in attrs
(time/level/msg/source) — guarded explicitly so a stray ExtraKeys
entry cannot break log pipelines.
- godoc: NewLogger documents the key-based-only limitation (msg-string
interpolation and slog.Any structs are not redacted).
Review loop: architect-reviewer PASS, code-reviewer NITS,
security-auditor PASS (2 MED addressed above), contract-guardian PASS.
Tests: pkg/observe/redact_test.go — default-key redaction, case
-insensitivity, value-type independence, group nesting, WithContext /
With() paths, ExtraKeys, custom placeholder, disabled, built-in
collision guard, DefaultRedactedKeys copy semantics. Full go test ./...
and contract freeze green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 task
jcsvwinston
added a commit
that referenced
this pull request
May 15, 2026
Session End Protocol for the structured-logger secret-redaction iteration (PR #62, ADR-007). State-files only — no code. - Archive the iteration at docs/iterations/2026-05-14-slog-secret-redaction.md, including the review-loop outcome (2 MED security findings folded into #62) and four small follow-ups. - Reset CURRENT_ITERATION.md to an empty slate; live-DB integration tests for App.AutoMigrate is the top-ranked next candidate. - Refresh HANDOFF.md: main @ 731de30, no active iteration, open housekeeping carried forward. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the secrets-in-logs gap from the 2026-05-14 post-sprint audit §7 item 6 — the sibling security item to ADR-006's CSRF hardening. Designed in ADR-007.
observe.NewLoggerbuilt aslog.Handlerwith noReplaceAttr— any code that logged a secret-bearing attribute (authorization,password,token, a sessioncookie, …) emitted it verbatim to the log sink.NewLoggernow redacts by default: the value of any attribute whose key is in a curated, case-insensitive denylist is replaced withRedactionPlaceholder([REDACTED]). Key and log-line shape are unchanged. Pure stdlib; no new dependency.API (additive — contract baseline updated)
observe.NewLoggerWithRedaction(level, format string, RedactionConfig)— explicit-control constructor.RedactionConfig{Disabled, ExtraKeys, Placeholder}.observe.DefaultRedactedKeys()— exposes the built-in denylist for runtime auditing.observe.RedactionPlaceholder— the default masked value.NewLoggerkeeps its exact signature and delegates toNewLoggerWithRedactionwith a zero-value config.log_redact_extra_keysconfig key (lifecycletransitional) threadsExtraKeysthroughApp.New.There is deliberately no config key to disable redaction — turning it off requires a code-level opt-out via
NewLoggerWithRedaction, so the decision surfaces in code review (the ADR-004 /WithOpenAuthzdiscipline).pkg/observestable surface)A deployment that intentionally logged a field under a denylisted key (e.g. an opaque non-secret named
token) now sees[REDACTED]there. Documented inCHANGELOG.mdunderChanged; contract-guardian confirmed noDEP-entry is needed (no symbol removed or renamed) — same governance trail as ADR-006.Review-loop fixes applied in this PR
architect-reviewer PASS, code-reviewer NITS, security-auditor PASS (2 MED — both addressed below), contract-guardian PASS.
"password"— now[REDACTED], which would lock the operator out on first boot. It is now written once to stderr, deliberately bypassing the logger; the structured log records only that it happened. (pkg/app/app.go)database_url,dsn,redis_url,connection_string, …),smtp_pass/smtp_password,aws_secret_access_key,aws_session_token, private-key-material names, provider tokens (oauth_token,github_token,slack_token, …).ExtraKeyscan no longer silence slog's built-in attrs (time/level/msg/source) — guarded explicitly so a stray entry cannot break log pipelines.NewLoggernow states redaction is key-based only: a secret interpolated into themsgstring, or nested in a struct logged viaslog.Anyunder a benign key, is not redacted.Test plan
pkg/observe/redact_test.go— default-key redaction, case-insensitivity, non-string value types,slog.Groupnesting,WithContext/With()paths,ExtraKeys, custom placeholder,Disabled, built-in-collision guard,DefaultRedactedKeyssorted + copy-semantics, built-in attrs pass through.go test ./...— clean.go test ./contracts/— freeze green; 7 newpkg/observesymbols +log_redact_extra_keys[]config key added to the baselines, correctly sorted.go vet ./pkg/observe/ ./pkg/app/— clean.🤖 Generated with Claude Code