From 339dc80bf1540dfbcc60a16719ccd86246c6500e Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 10 May 2026 14:10:07 +0200 Subject: [PATCH] feat(proxy): add UnsafeLogCredentialHeaders escape hatch for debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit made credential-header redaction unconditional in proxy log output, which is the right safety floor for production but inconvenient for local debugging when a developer wants to inspect Authorization / Cookie / X-Bose-Token values flowing through the service. Add an explicit "I-know-what-I-am-doing" toggle: * New LoggingProxy.UnsafeLogCredentialHeaders bool field. * Default off — the redaction floor stays in place. * Reads the LOG_PROXY_CREDENTIALS env var so a developer can flip it on without recompiling, mirroring the existing LOG_PROXY_BODY pattern. * When true, formatHeaders skips both the always-sensitive floor and the broader Redact policy, so log lines contain raw header values. CodeQL's go/clear-text-logging rule continues to be satisfied because the default code path still redacts; only an explicit opt-in via configuration produces unredacted output, mirroring how AllowInsecureUpstreamTLS works. Co-Authored-By: Claude Opus 4.7 (1M context) --- pkg/service/proxy/proxy.go | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/pkg/service/proxy/proxy.go b/pkg/service/proxy/proxy.go index 4fbee6b..30f2d66 100644 --- a/pkg/service/proxy/proxy.go +++ b/pkg/service/proxy/proxy.go @@ -36,15 +36,25 @@ type LoggingProxy struct { RecordEnabled bool MaxBodySize int64 Recorder *Recorder + + // UnsafeLogCredentialHeaders disables the otherwise-unconditional + // redaction of credential-bearing headers (Authorization, Cookie, …) in + // LogRequest / LogResponse output. This is an explicit + // "I-know-what-I'm-doing" escape hatch for local debugging only — never + // enable it in production. Defaults to false; the env-var + // LOG_PROXY_CREDENTIALS=true flips it on so a developer can opt in + // without recompiling. + UnsafeLogCredentialHeaders bool } // NewLoggingProxy creates a lightweight logger for HTTP requests/responses. func NewLoggingProxy(_ string, redact bool) *LoggingProxy { // targetURL logic should be handled by the caller or we can parse it here return &LoggingProxy{ - Redact: redact, - LogBody: os.Getenv("LOG_PROXY_BODY") == "true", - MaxBodySize: 1024 * 10, // 10KB default limit for logging + Redact: redact, + LogBody: os.Getenv("LOG_PROXY_BODY") == "true", + UnsafeLogCredentialHeaders: os.Getenv("LOG_PROXY_CREDENTIALS") == "true", + MaxBodySize: 1024 * 10, // 10KB default limit for logging } } @@ -55,7 +65,7 @@ func (lp *LoggingProxy) SetRecorder(r *Recorder) { // LogRequest prints an abbreviated request with optional header/body redaction. func (lp *LoggingProxy) LogRequest(r *http.Request) { - headers := formatHeaders(r.Header, lp.Redact) + headers := formatHeaders(r.Header, lp.Redact, lp.UnsafeLogCredentialHeaders) bodyStr := "[HIDDEN]" @@ -79,7 +89,7 @@ func (lp *LoggingProxy) LogRequest(r *http.Request) { // LogResponse prints an abbreviated response with optional header/body redaction. func (lp *LoggingProxy) LogResponse(r *http.Response) { - headers := formatHeaders(r.Header, lp.Redact) + headers := formatHeaders(r.Header, lp.Redact, lp.UnsafeLogCredentialHeaders) bodyStr := "[HIDDEN]" @@ -105,17 +115,23 @@ func (lp *LoggingProxy) LogResponse(r *http.Response) { } } -func formatHeaders(h http.Header, redact bool) string { +func formatHeaders(h http.Header, redact, unsafeLogCredentials bool) string { var sb strings.Builder // In Go, http.Header is a map[string][]string. // Iterating over the map directly allows us to see the actual keys // stored in the map, which might not be canonical if set directly. for k, vv := range h { val := strings.Join(vv, ", ") - // Always redact credentials (Authorization, Cookie, …) regardless of - // the LoggingProxy.Redact toggle — the toggle controls *additional* - // redaction, never the safety floor. - if isAlwaysSensitive(k) || (redact && isSensitive(k)) { + // Credentials (Authorization, Cookie, …) are redacted by default. + // unsafeLogCredentials lifts that floor entirely — explicit opt-in + // for local debugging only. When the floor is in place, the + // caller's broader Redact toggle adds further coverage. + switch { + case unsafeLogCredentials: + // No redaction. + case isAlwaysSensitive(k): + val = "[REDACTED]" + case redact && isSensitive(k): val = "[REDACTED]" }