From a21a11af78f51b8edc8ce224ec7c7f53391796ef Mon Sep 17 00:00:00 2001 From: Richard Clark Date: Mon, 6 Oct 2025 14:01:12 +0100 Subject: [PATCH] feat: add configurable header redaction for logs Add support for log.redact_headers configuration option to redact additional sensitive headers beyond the defaults (authorization, cookie, set-cookie, x-session-token). Headers specified in this configuration will have their values masked in log output. Configuration can be set via YAML: log: redact_headers: - x-custom-authorization - x-api-key This prevents sensitive authentication headers from being logged, improving security for deployments using non-standard auth headers. --- driver/configuration/config_keys.go | 5 ++ driver/configuration/provider.go | 2 + driver/configuration/provider_koanf.go | 4 ++ .../provider_koanf_public_test.go | 45 ++++++++++++ .../testdata/log_redact_headers_config.yaml | 4 ++ driver/registry_memory.go | 2 +- driver/registry_memory_test.go | 69 +++++++++++++++++++ 7 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 driver/configuration/testdata/log_redact_headers_config.yaml diff --git a/driver/configuration/config_keys.go b/driver/configuration/config_keys.go index 88e725ba06..97830f1911 100644 --- a/driver/configuration/config_keys.go +++ b/driver/configuration/config_keys.go @@ -81,3 +81,8 @@ const ( ErrorsRedirectIsEnabled Key = ErrorsHandlers + ".redirect.enabled" ErrorsWWWAuthenticateIsEnabled Key = ErrorsHandlers + ".www_authenticate.enabled" ) + +// Log +const ( + LogRedactHeaders Key = "log.redact_headers" +) diff --git a/driver/configuration/provider.go b/driver/configuration/provider.go index 4293d97092..f5a3d048d4 100644 --- a/driver/configuration/provider.go +++ b/driver/configuration/provider.go @@ -79,6 +79,8 @@ type Provider interface { TLSConfig(daemon string) *TLSConfig + LogRedactHeaders() []string + SetForTest(t testing.TB, key string, value interface{}) } diff --git a/driver/configuration/provider_koanf.go b/driver/configuration/provider_koanf.go index d3933eac57..a51f50c0ab 100644 --- a/driver/configuration/provider_koanf.go +++ b/driver/configuration/provider_koanf.go @@ -435,6 +435,10 @@ func (v *KoanfProvider) TracingConfig() *otelx.Config { return v.source.TracingConfig(v.TracingServiceName()) } +func (v *KoanfProvider) LogRedactHeaders() []string { + return v.source.StringsF(LogRedactHeaders, []string{}) +} + func (v *KoanfProvider) PrometheusHideRequestPaths() bool { return v.source.BoolF(PrometheusServeHideRequestPaths, false) } diff --git a/driver/configuration/provider_koanf_public_test.go b/driver/configuration/provider_koanf_public_test.go index 1e39474c76..62b780d512 100644 --- a/driver/configuration/provider_koanf_public_test.go +++ b/driver/configuration/provider_koanf_public_test.go @@ -453,3 +453,48 @@ func TestAuthenticatorOAuth2TokenIntrospectionPreAuthorization(t *testing.T) { }) } } + +func TestLogRedactHeaders(t *testing.T) { + t.Run("case=returns empty array when not configured", func(t *testing.T) { + p, err := configuration.NewKoanfProvider( + context.Background(), + nil, + logrusx.New("", ""), + ) + require.NoError(t, err) + + headers := p.LogRedactHeaders() + assert.Empty(t, headers) + }) + + t.Run("case=returns configured headers", func(t *testing.T) { + p, err := configuration.NewKoanfProvider( + context.Background(), + nil, + logrusx.New("", ""), + configx.SkipValidation(), + configx.WithValues(map[string]interface{}{ + "log.redact_headers": []string{"x-custom-authorization", "x-api-key"}, + }), + ) + require.NoError(t, err) + + headers := p.LogRedactHeaders() + assert.Equal(t, []string{"x-custom-authorization", "x-api-key"}, headers) + }) + + t.Run("case=reads from config file", func(t *testing.T) { + p, err := configuration.NewKoanfProvider( + context.Background(), + nil, + logrusx.New("", ""), + configx.SkipValidation(), + configx.WithConfigFiles("testdata/log_redact_headers_config.yaml"), + ) + require.NoError(t, err) + + headers := p.LogRedactHeaders() + assert.Contains(t, headers, "x-custom-authorization") + assert.Contains(t, headers, "x-api-key") + }) +} diff --git a/driver/configuration/testdata/log_redact_headers_config.yaml b/driver/configuration/testdata/log_redact_headers_config.yaml new file mode 100644 index 0000000000..3e65ca9a3f --- /dev/null +++ b/driver/configuration/testdata/log_redact_headers_config.yaml @@ -0,0 +1,4 @@ +log: + redact_headers: + - x-custom-authorization + - x-api-key diff --git a/driver/registry_memory.go b/driver/registry_memory.go index bcf3d57200..519ef4d6f4 100644 --- a/driver/registry_memory.go +++ b/driver/registry_memory.go @@ -177,7 +177,7 @@ func (r *RegistryMemory) Writer() herodot.Writer { func (r *RegistryMemory) Logger() *logrusx.Logger { if r.logger == nil { - r.logger = logrusx.New("ORY Oathkeeper", x.Version) + r.logger = logrusx.New("ORY Oathkeeper", x.Version, logrusx.WithAdditionalRedactedHeaders(r.c.LogRedactHeaders())) } return r.logger } diff --git a/driver/registry_memory_test.go b/driver/registry_memory_test.go index 11f2765be3..72056959ec 100644 --- a/driver/registry_memory_test.go +++ b/driver/registry_memory_test.go @@ -5,12 +5,14 @@ package driver import ( "context" + "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/oathkeeper/driver/configuration" + "github.com/ory/x/configx" "github.com/ory/x/logrusx" ) @@ -50,3 +52,70 @@ func TestRegistryMemoryPipelineAuthorizer(t *testing.T) { }) } } + +func TestRegistryLoggerRedactsConfiguredHeaders(t *testing.T) { + t.Run("case=logger redacts custom headers from configuration", func(t *testing.T) { + c, err := configuration.NewKoanfProvider( + context.Background(), + nil, + logrusx.New("", ""), + configx.SkipValidation(), + configx.WithValues(map[string]interface{}{ + "log.redact_headers": []string{"x-custom-authorization", "x-api-key"}, + }), + ) + require.NoError(t, err) + + r := NewRegistry(c) + logger := r.Logger() + + // Create headers with custom auth header + headers := http.Header{} + headers.Set("Authorization", "Bearer default-token") + headers.Set("X-Custom-Authorization", "Bearer custom-token") + headers.Set("X-API-Key", "secret-key") + headers.Set("X-Request-ID", "request-123") + + // Get redacted headers + redacted := logger.HTTPHeadersRedacted(headers) + + // Default sensitive headers should be redacted + assert.Contains(t, redacted["authorization"], "Value is sensitive") + + // Custom configured headers should be redacted + assert.Contains(t, redacted["x-custom-authorization"], "Value is sensitive") + assert.Contains(t, redacted["x-api-key"], "Value is sensitive") + + // Non-sensitive headers should not be redacted + assert.Equal(t, "request-123", redacted["x-request-id"]) + }) + + t.Run("case=logger without custom configuration only redacts default headers", func(t *testing.T) { + c, err := configuration.NewKoanfProvider( + context.Background(), + nil, + logrusx.New("", ""), + configx.SkipValidation(), + ) + require.NoError(t, err) + + r := NewRegistry(c) + logger := r.Logger() + + // Create headers with custom auth header + headers := http.Header{} + headers.Set("Authorization", "Bearer default-token") + headers.Set("X-Custom-Authorization", "Bearer custom-token") + headers.Set("X-API-Key", "secret-key") + + // Get redacted headers + redacted := logger.HTTPHeadersRedacted(headers) + + // Default sensitive headers should be redacted + assert.Contains(t, redacted["authorization"], "Value is sensitive") + + // Custom headers should NOT be redacted (no configuration) + assert.Equal(t, "Bearer custom-token", redacted["x-custom-authorization"]) + assert.Equal(t, "secret-key", redacted["x-api-key"]) + }) +}