From b2f76561f1927a8b0cc36f24c3cc6a3377099e6d Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:13:56 +0000 Subject: [PATCH 1/2] Forward safe Caddy access attributes --- lib/ingress/logs.go | 59 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/ingress/logs.go b/lib/ingress/logs.go index f085b039a..82181a401 100644 --- a/lib/ingress/logs.go +++ b/lib/ingress/logs.go @@ -3,7 +3,9 @@ package ingress import ( "bufio" "context" + "crypto/sha256" "encoding/json" + "fmt" "log/slog" "os/exec" "strings" @@ -84,6 +86,23 @@ type caddyLogEntry struct { Error string `json:"error,omitempty"` Module string `json:"module,omitempty"` Adapter string `json:"adapter,omitempty"` + Request *struct { + Host string `json:"host"` + Method string `json:"method"` + } `json:"request"` + Status int `json:"status"` + Size int64 `json:"size"` + BytesRead int64 `json:"bytes_read"` + Duration float64 `json:"duration"` +} + +func accessMethod(method string) string { + switch method { + case "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "CONNECT", "TRACE": + return method + default: + return "OTHER" + } } // forwardLogLine parses a JSON log line and forwards to OTEL logger. @@ -99,8 +118,7 @@ func (f *CaddyLogForwarder) forwardLogLine(ctx context.Context, line string) { var entry caddyLogEntry if err := json.Unmarshal([]byte(line), &entry); err != nil { - // If we can't parse, keep raw line at debug to avoid info noise. - f.logger.DebugContext(ctx, "caddy: "+line) + f.logger.DebugContext(ctx, "caddy: invalid JSON log entry") return } @@ -112,18 +130,37 @@ func (f *CaddyLogForwarder) forwardLogLine(ctx context.Context, line string) { "caddy_logger", entry.Logger, "caddy_ts", ts.Format(time.RFC3339Nano), } - if entry.Module != "" { - attrs = append(attrs, "module", entry.Module) - } - if entry.Adapter != "" { - attrs = append(attrs, "adapter", entry.Adapter) - } - if entry.Error != "" { - attrs = append(attrs, "error", entry.Error) + access := entry.Logger == "http.log.access" + msg := "caddy: " + entry.Msg + if access { + msg = "caddy: handled request" + if entry.Request != nil { + attrs = append(attrs, + "http_method", accessMethod(entry.Request.Method), + "http_status", entry.Status, + "duration_seconds", entry.Duration, + "bytes_written", entry.Size, + "bytes_read", entry.BytesRead, + ) + if entry.Request.Host != "" { + // Preserve correlation without forwarding the caller-controlled host. + hostHash := sha256.Sum256([]byte(strings.ToLower(entry.Request.Host))) + attrs = append(attrs, "http_host_sha256", fmt.Sprintf("%x", hostHash)) + } + } + } else { + if entry.Module != "" { + attrs = append(attrs, "module", entry.Module) + } + if entry.Adapter != "" { + attrs = append(attrs, "adapter", entry.Adapter) + } + if entry.Error != "" { + attrs = append(attrs, "error", entry.Error) + } } // Forward with appropriate level - msg := "caddy: " + entry.Msg switch strings.ToLower(entry.Level) { case "debug": f.logger.DebugContext(ctx, msg, attrs...) From 1e655b78e3724a8dbc6586a6d88ee3c4c7c9af2b Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:13:56 +0000 Subject: [PATCH 2/2] Cover Caddy access logging and redaction --- lib/ingress/logs_test.go | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 lib/ingress/logs_test.go diff --git a/lib/ingress/logs_test.go b/lib/ingress/logs_test.go new file mode 100644 index 000000000..1f1bf6dad --- /dev/null +++ b/lib/ingress/logs_test.go @@ -0,0 +1,62 @@ +package ingress + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "log/slog" + "testing" + + "github.com/stretchr/testify/require" +) + +func forwardedLog(t *testing.T, line string) (map[string]any, string) { + t.Helper() + var output bytes.Buffer + forwarder := CaddyLogForwarder{ + logger: slog.New(slog.NewJSONHandler(&output, &slog.HandlerOptions{Level: slog.LevelDebug})), + } + forwarder.forwardLogLine(context.Background(), line) + var record map[string]any + require.NoError(t, json.Unmarshal(output.Bytes(), &record)) + return record, output.String() +} + +func TestForwardLogLineAccessEntry(t *testing.T) { + host := "session.example.test" + entry := `{"level":"info","ts":1788907156.1,"logger":"http.log.access","msg":"message-secret","error":"error-secret","request":{"method":"GET","host":"session.example.test","uri":"/live/path-secret?token=query-secret","client_ip":"192.0.2.1","headers":{"Authorization":["Bearer header-secret"],"Cookie":["session=cookie-secret"]}},"status":502,"size":34,"bytes_read":12,"duration":0.125}` + record, output := forwardedLog(t, entry) + + require.Equal(t, "GET", record["http_method"]) + require.Equal(t, float64(502), record["http_status"]) + require.Equal(t, float64(34), record["bytes_written"]) + require.Equal(t, float64(12), record["bytes_read"]) + require.Equal(t, 0.125, record["duration_seconds"]) + hostHash := sha256.Sum256([]byte(host)) + require.Equal(t, fmt.Sprintf("%x", hostHash), record["http_host_sha256"]) + for _, sensitive := range []string{host, "192.0.2.1", "path-secret", "query-secret", "header-secret", "cookie-secret", "message-secret", "error-secret"} { + require.NotContains(t, output, sensitive) + } + require.NotContains(t, record, "http_path") + require.NotContains(t, record, "client_ip") +} + +func TestForwardLogLineBoundsMethodAndNonAccessFields(t *testing.T) { + record, output := forwardedLog(t, `{"level":"info","logger":"http.log.access","msg":"handled request","request":{"method":"secret-method","host":""},"status":200}`) + require.Equal(t, "OTHER", record["http_method"]) + require.NotContains(t, record, "http_host_sha256") + require.NotContains(t, output, "secret-method") + + record, _ = forwardedLog(t, `{"level":"info","logger":"admin","msg":"config loaded","request":{"method":"GET","host":"session.example.test"},"status":200}`) + require.Equal(t, "admin", record["caddy_logger"]) + require.NotContains(t, record, "http_status") + require.NotContains(t, record, "http_host_sha256") +} + +func TestForwardLogLineInvalidJSONDoesNotForwardRawLine(t *testing.T) { + record, output := forwardedLog(t, `{"request":{"headers":{"Authorization":"secret"}`) + require.Equal(t, "caddy: invalid JSON log entry", record["msg"]) + require.NotContains(t, output, "secret") +}