Skip to content

feat(observe): redact secrets in the structured logger by default#62

Merged
jcsvwinston merged 2 commits into
mainfrom
feature/slog-secrets-redaction
May 14, 2026
Merged

feat(observe): redact secrets in the structured logger by default#62
jcsvwinston merged 2 commits into
mainfrom
feature/slog-secrets-redaction

Conversation

@jcsvwinston
Copy link
Copy Markdown
Owner

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.NewLogger built a slog.Handler with no ReplaceAttr — any code that logged a secret-bearing attribute (authorization, password, token, a session cookie, …) emitted it verbatim to the log sink. NewLogger now redacts by default: the value of any attribute whose key is in a curated, case-insensitive denylist is replaced with RedactionPlaceholder ([REDACTED]). Key and log-line shape are unchanged. Pure stdlib; no new dependency.

Note: stacks on the open state-close PR #61 and the CSRF PR's CHANGELOG section. If #61 merges first the rebase is trivial (state files only).

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.
  • NewLogger keeps its exact signature and delegates to NewLoggerWithRedaction with a zero-value config.
  • log_redact_extra_keys config key (lifecycle transitional) threads ExtraKeys through App.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 / WithOpenAuthz discipline).

⚠️ Behaviour change (pre-v1.0, pkg/observe stable 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 in CHANGELOG.md under Changed; contract-guardian confirmed no DEP- 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.

  • security MED — bootstrap-password lockout. The auto-generated admin bootstrap password was logged under the key "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)
  • security MED — denylist expansion. Added framework-relevant keys: DSN / connection strings (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, …).
  • code-review — built-in collision guard. ExtraKeys can no longer silence slog's built-in attrs (time/level/msg/source) — guarded explicitly so a stray entry cannot break log pipelines.
  • godoc — limitations documented. NewLogger now states redaction is key-based only: a secret interpolated into the msg string, or nested in a struct logged via slog.Any under a benign key, is not redacted.

Test plan

  • pkg/observe/redact_test.go — default-key redaction, case-insensitivity, non-string value types, slog.Group nesting, WithContext / With() paths, ExtraKeys, custom placeholder, Disabled, built-in-collision guard, DefaultRedactedKeys sorted + copy-semantics, built-in attrs pass through.
  • go test ./... — clean.
  • go test ./contracts/ — freeze green; 7 new pkg/observe symbols + log_redact_extra_keys[] config key added to the baselines, correctly sorted.
  • go vet ./pkg/observe/ ./pkg/app/ — clean.

🤖 Generated with Claude Code

jcsvwinston and others added 2 commits May 14, 2026 17:01
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>
@jcsvwinston jcsvwinston merged commit f56032e into main May 14, 2026
9 checks passed
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>
@jcsvwinston jcsvwinston deleted the feature/slog-secrets-redaction branch May 15, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant