enhancement: audit-channel hook on every secret.Get (ai) #5

Open
opened 2026-05-31 23:55:12 +02:00 by heiko · 0 comments
Owner

enhancement: audit-channel hook on every secret.Get (ai)

Summary

secret.Get(spec) resolves credentials silently. There is no
hook for emitting an audit-channel record naming the
resolution: which scheme was used, which fallback candidate
won, success or failure, the OS-level reason on failure
(file-not-found, env-unset, mode-too-permissive). Consumers
that need security-event review for credential access have to
wrap every call site themselves.

Proposed addition

A package-level audit-logger hook, similar to the standard-
library slog.Default() pattern:

// SetAuditLogger installs a logger that receives one record per
// Get call. Setting nil disables audit emission (default).
//
// The audit record carries:
//   - msg = "resolved"
//   - scheme = the resolved scheme (env, file, plain, ...)
//   - status = "success" | "failed"
//   - reason = a short error category on failure
//     (e.g. "missing", "permission", "unknown")
//
// Resolved cleartext is NEVER part of the record.
func SetAuditLogger(l *slog.Logger)

Or, if a global is too implicit for the project's taste, a
context-carried logger:

func WithAuditLogger(ctx context.Context, l *slog.Logger) context.Context
func GetWithAudit(ctx context.Context, spec string) (string, error)

I lean global. secret.Get is intentionally a low-ceremony API;
threading a context through every call site for an
observability concern adds a lot of boilerplate.

Why this belongs upstream

Security-conscious deployments typically have a security-review
step that asks "what credentials were resolved when this process
started, and which scheme did each come from?" Today every
consumer either:

  • adds explicit audit emission at every Get call (boilerplate
    • easy to miss a call site), or
  • doesn't audit (the thing the security review wanted is absent).

Upstream emission means consumers get the audit trail "for
free" by installing a logger once at startup.

The dmarc project requires this in spec 1.2 §"Audit channel"
(every credential resolution emits a record on the
unfilterable audit channel). dmarc wrapped secret.Get in
internal/secret/resolver.go to add this; ~160 LOC of which
~40 LOC is the audit emission.

Compatibility

Purely additive. Default behavior (nil logger / no
WithAuditLogger) is the current silent behavior. Existing
callers see no change.

What dmarc does today

internal/secret/resolver.go wraps secret.Get with:

  1. an slog-based audit emission (level=audit component=secret phase=runtime msg=resolved scheme=... status=...),
  2. a singleflight-style dedup so concurrent resolves of the same
    spec emit one record per unique resolution, not one per
    goroutine.

(2) is dmarc-specific and not part of this request — keeping
the audit hook minimal so consumers who don't want
deduplication aren't forced into it.

References

# enhancement: audit-channel hook on every `secret.Get` (ai) ## Summary `secret.Get(spec)` resolves credentials silently. There is no hook for emitting an audit-channel record naming the resolution: which scheme was used, which fallback candidate won, success or failure, the OS-level reason on failure (file-not-found, env-unset, mode-too-permissive). Consumers that need security-event review for credential access have to wrap every call site themselves. ## Proposed addition A package-level audit-logger hook, similar to the standard- library `slog.Default()` pattern: ```go // SetAuditLogger installs a logger that receives one record per // Get call. Setting nil disables audit emission (default). // // The audit record carries: // - msg = "resolved" // - scheme = the resolved scheme (env, file, plain, ...) // - status = "success" | "failed" // - reason = a short error category on failure // (e.g. "missing", "permission", "unknown") // // Resolved cleartext is NEVER part of the record. func SetAuditLogger(l *slog.Logger) ``` Or, if a global is too implicit for the project's taste, a context-carried logger: ```go func WithAuditLogger(ctx context.Context, l *slog.Logger) context.Context func GetWithAudit(ctx context.Context, spec string) (string, error) ``` I lean global. `secret.Get` is intentionally a low-ceremony API; threading a context through every call site for an observability concern adds a lot of boilerplate. ## Why this belongs upstream Security-conscious deployments typically have a security-review step that asks "what credentials were resolved when this process started, and which scheme did each come from?" Today every consumer either: - adds explicit audit emission at every `Get` call (boilerplate + easy to miss a call site), or - doesn't audit (the thing the security review wanted is absent). Upstream emission means consumers get the audit trail "for free" by installing a logger once at startup. The dmarc project requires this in spec 1.2 §"Audit channel" (every credential resolution emits a record on the unfilterable audit channel). dmarc wrapped `secret.Get` in `internal/secret/resolver.go` to add this; ~160 LOC of which ~40 LOC is the audit emission. ## Compatibility Purely additive. Default behavior (nil logger / no `WithAuditLogger`) is the current silent behavior. Existing callers see no change. ## What dmarc does today `internal/secret/resolver.go` wraps `secret.Get` with: 1. an `slog`-based audit emission (`level=audit component=secret phase=runtime msg=resolved scheme=... status=...`), 2. a singleflight-style dedup so concurrent resolves of the same spec emit one record per unique resolution, not one per goroutine. (2) is dmarc-specific and not part of this request — keeping the audit hook minimal so consumers who don't want deduplication aren't forced into it. ## References - dmarc spec 1.2 §"Audit channel": https://git.schlittermann.de/heiko/dmarc/src/branch/dev/docs/specs/1.2-config-and-logging.md - dmarc wrapper: https://git.schlittermann.de/heiko/dmarc/src/branch/dev/internal/secret/resolver.go
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
heiko/secret#5
No description provided.