Skip to content

Commit 3445fa9

Browse files
feat: request and response body redaction for sensitive information (#1758)
**Description** Implements redaction of sensitive information in debug logs to prevent exposure of user prompts, API keys, AI-generated content. The redaction system preserves json-structure for requests/responses and includes length/hash metadata for debugging purposes. **Problem** Debug logging currently exposes sensitive information including: - User prompts and messages (text, images, audio, files) - AI-generated responses and tool call arguments - API keys and authorization tokens in headers - Tool definitions, function schemas, and response format schemas - Prediction content and guided JSON schemas **Solution** **Request Body Redaction** Added `RedactSensitiveInfoFromRequest` interface method to `EndpointSpec` with full implementation for chat completions endpoint (placeholders for others): **Redacted Content:** - **Messages**: All message types (user, assistant, system, developer, tool) - Text content with length and hash placeholders - Images (URLs and base64 data) - Audio data (base64-encoded) - File attachments - **Tool Definitions**: Function descriptions and parameter schemas - **Tool Call Arguments**: Function names and arguments in assistant messages - **Response Formats**: JSON schema names, descriptions, and schema definitions - **Guided JSON**: Raw JSON schema content - **Prediction Content**: Cached prompts and prefill content **Redaction Format:** ``` [REDACTED LENGTH=142 HASH=a3f5e8c2] ``` The hash (SHA256) enables: - Correlating identical content across requests for cache debugging - Identifying duplicate requests without content exposure - Matching redacted logs to specific content patterns **Example** **Before** (debug log exposes sensitive content): ```shell time=2026-01-12T11:39:01.848-05:00 level=DEBUG msg="request body processing" request_id=suku-234 is_upstream_filter=false request="request_body:{body:\"{\\n \\\"model\\\": \\\"gcp.gemini-2.5-flash\\\",\\n \\\"messages\\\": [\\n {\\n \\\"role\\\": \\\"user\\\",\\n \\\"content\\\": \\\"What is capital of France?\\\"\\n }\\n ],\\n \\\"max_completion_tokens\\\": 101,\\n \\\"stream\\\": false\\n}\" end_of_stream:true} metadata_context:{}" ``` **After** (redacted with debugging metadata): ```shell time=2026-01-12T16:36:35.689-05:00 level=DEBUG msg="request body processing" request_id=suku-234 is_upstream_filter=false request="{\"messages\":[{\"content\":\"[REDACTED LENGTH=26 HASH=e3b235f0]\",\"role\":\"user\"}],\"model\":\"gcp.gemini-2.5-flash\",\"max_completion_tokens\":101}" ``` ## Special notes for reviewers - Declaration (as per point 4 [1] ): AI tool was used to generate part of the code in this PR. Open to suggestion from community :) 1: https://github.com/envoyproxy/ai-gateway/blob/953951fb5c9cafc7e1a8747c64b13cff291fb1ce/CONTRIBUTING.md#what-is-allowed --------- Signed-off-by: Sukumar Gaonkar <[email protected]> Signed-off-by: Dan Sun <[email protected]> Co-authored-by: Dan Sun <[email protected]>
1 parent b5e5f87 commit 3445fa9

29 files changed

+2288
-188
lines changed

cmd/controller/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838

3939
type flags struct {
4040
extProcLogLevel string
41+
extProcEnableRedaction bool
4142
extProcImage string
4243
extProcImagePullPolicy corev1.PullPolicy
4344
enableLeaderElection bool
@@ -106,6 +107,11 @@ func parseAndValidateFlags(args []string) (*flags, error) {
106107
"info",
107108
"The log level for the external processor. One of 'debug', 'info', 'warn', or 'error'.",
108109
)
110+
extProcEnableRedactionPtr := fs.Bool(
111+
"extProcEnableRedaction",
112+
false,
113+
"Enable redaction of sensitive information in debug logs for the external processor.",
114+
)
109115
extProcImagePtr := fs.String(
110116
"extProcImage",
111117
"docker.io/envoyproxy/ai-gateway-extproc:latest",
@@ -306,6 +312,7 @@ func parseAndValidateFlags(args []string) (*flags, error) {
306312

307313
return &flags{
308314
extProcLogLevel: *extProcLogLevelPtr,
315+
extProcEnableRedaction: *extProcEnableRedactionPtr,
309316
extProcImage: *extProcImagePtr,
310317
extProcImagePullPolicy: extProcPullPolicy,
311318
enableLeaderElection: *enableLeaderElectionPtr,
@@ -410,6 +417,7 @@ func main() {
410417
ExtProcImage: parsedFlags.extProcImage,
411418
ExtProcImagePullPolicy: parsedFlags.extProcImagePullPolicy,
412419
ExtProcLogLevel: parsedFlags.extProcLogLevel,
420+
ExtProcEnableRedaction: parsedFlags.extProcEnableRedaction,
413421
EnableLeaderElection: parsedFlags.enableLeaderElection,
414422
UDSPath: extProcUDSPath,
415423
RequestHeaderAttributes: parsedFlags.requestHeaderAttributes,

cmd/controller/main_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func Test_parseAndValidateFlags(t *testing.T) {
2323
t.Run("no flags", func(t *testing.T) {
2424
f, err := parseAndValidateFlags([]string{})
2525
require.Equal(t, "info", f.extProcLogLevel)
26+
require.False(t, f.extProcEnableRedaction)
2627
require.Equal(t, "docker.io/envoyproxy/ai-gateway-extproc:latest", f.extProcImage)
2728
require.Equal(t, corev1.PullIfNotPresent, f.extProcImagePullPolicy)
2829
require.True(t, f.enableLeaderElection)
@@ -47,6 +48,7 @@ func Test_parseAndValidateFlags(t *testing.T) {
4748
t.Run(tc.name, func(t *testing.T) {
4849
args := []string{
4950
tc.dash + "extProcLogLevel=debug",
51+
tc.dash + "extProcEnableRedaction=true",
5052
tc.dash + "extProcImage=example.com/extproc:latest",
5153
tc.dash + "extProcImagePullPolicy=Always",
5254
tc.dash + "enableLeaderElection=false",
@@ -67,6 +69,7 @@ func Test_parseAndValidateFlags(t *testing.T) {
6769
}
6870
f, err := parseAndValidateFlags(args)
6971
require.Equal(t, "debug", f.extProcLogLevel)
72+
require.True(t, f.extProcEnableRedaction)
7073
require.Equal(t, "example.com/extproc:latest", f.extProcImage)
7174
require.Equal(t, corev1.PullAlways, f.extProcImagePullPolicy)
7275
require.False(t, f.enableLeaderElection)

cmd/extproc/mainlib/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type extProcFlags struct {
4242
configPath string // path to the configuration file.
4343
extProcAddr string // gRPC address for the external processor.
4444
logLevel slog.Level // log level for the external processor.
45+
enableRedaction bool // enable redaction of sensitive information in debug logs.
4546
adminPort int // HTTP port for the admin server (metrics and health).
4647
requestHeaderAttributes *string // comma-separated key-value pairs for mapping HTTP request headers to otel attributes shared across metrics, spans, and access logs.
4748
spanRequestHeaderAttributes *string // comma-separated key-value pairs for mapping HTTP request headers to otel span attributes.
@@ -92,6 +93,8 @@ func parseAndValidateFlags(args []string) (extProcFlags, error) {
9293
"info",
9394
"log level for the external processor. One of 'debug', 'info', 'warn', or 'error'.",
9495
)
96+
fs.BoolVar(&flags.enableRedaction, "enableRedaction", false,
97+
"Enable redaction of sensitive information in debug logs.")
9598
fs.IntVar(&flags.adminPort, "adminPort", 1064, "HTTP port for the admin server (serves /metrics and /health endpoints).")
9699
fs.Func("requestHeaderAttributes",
97100
"Comma-separated key-value pairs for mapping HTTP request headers to otel attributes shared across metrics, spans, and access logs. Format: x-tenant-id:tenant.id.",
@@ -284,7 +287,7 @@ func Main(ctx context.Context, args []string, stderr io.Writer) (err error) {
284287

285288
extproc.LogRequestHeaderAttributes = logRequestHeaderAttributes
286289

287-
server, err := extproc.NewServer(l)
290+
server, err := extproc.NewServer(l, flags.enableRedaction)
288291
if err != nil {
289292
return fmt.Errorf("failed to create external processor server: %w", err)
290293
}

cmd/extproc/mainlib/main_test.go

Lines changed: 96 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -23,52 +23,67 @@ import (
2323
func Test_parseAndValidateFlags(t *testing.T) {
2424
t.Run("ok extProcFlags", func(t *testing.T) {
2525
for _, tc := range []struct {
26-
name string
27-
args []string
28-
configPath string
29-
addr string
30-
rootPrefix string
31-
logLevel slog.Level
26+
name string
27+
args []string
28+
configPath string
29+
addr string
30+
rootPrefix string
31+
logLevel slog.Level
32+
enableRedaction bool
3233
}{
3334
{
34-
name: "minimal extProcFlags",
35-
args: []string{"-configPath", "/path/to/config.yaml"},
36-
configPath: "/path/to/config.yaml",
37-
addr: ":1063",
38-
rootPrefix: "/",
39-
logLevel: slog.LevelInfo,
35+
name: "minimal extProcFlags",
36+
args: []string{"-configPath", "/path/to/config.yaml"},
37+
configPath: "/path/to/config.yaml",
38+
addr: ":1063",
39+
rootPrefix: "/",
40+
logLevel: slog.LevelInfo,
41+
enableRedaction: false,
4042
},
4143
{
42-
name: "custom addr",
43-
args: []string{"-configPath", "/path/to/config.yaml", "-extProcAddr", "unix:///tmp/ext_proc.sock"},
44-
configPath: "/path/to/config.yaml",
45-
addr: "unix:///tmp/ext_proc.sock",
46-
rootPrefix: "/",
47-
logLevel: slog.LevelInfo,
44+
name: "custom addr",
45+
args: []string{"-configPath", "/path/to/config.yaml", "-extProcAddr", "unix:///tmp/ext_proc.sock"},
46+
configPath: "/path/to/config.yaml",
47+
addr: "unix:///tmp/ext_proc.sock",
48+
rootPrefix: "/",
49+
logLevel: slog.LevelInfo,
50+
enableRedaction: false,
4851
},
4952
{
50-
name: "log level debug",
51-
args: []string{"-configPath", "/path/to/config.yaml", "-logLevel", "debug"},
52-
configPath: "/path/to/config.yaml",
53-
addr: ":1063",
54-
rootPrefix: "/",
55-
logLevel: slog.LevelDebug,
53+
name: "log level debug",
54+
args: []string{"-configPath", "/path/to/config.yaml", "-logLevel", "debug"},
55+
configPath: "/path/to/config.yaml",
56+
addr: ":1063",
57+
rootPrefix: "/",
58+
logLevel: slog.LevelDebug,
59+
enableRedaction: false,
5660
},
5761
{
58-
name: "log level warn",
59-
args: []string{"-configPath", "/path/to/config.yaml", "-logLevel", "warn"},
60-
configPath: "/path/to/config.yaml",
61-
addr: ":1063",
62-
rootPrefix: "/",
63-
logLevel: slog.LevelWarn,
62+
name: "log level debug with redaction enabled",
63+
args: []string{"-configPath", "/path/to/config.yaml", "-logLevel", "debug", "-enableRedaction"},
64+
configPath: "/path/to/config.yaml",
65+
addr: ":1063",
66+
rootPrefix: "/",
67+
logLevel: slog.LevelDebug,
68+
enableRedaction: true,
6469
},
6570
{
66-
name: "log level error",
67-
args: []string{"-configPath", "/path/to/config.yaml", "-logLevel", "error"},
68-
configPath: "/path/to/config.yaml",
69-
addr: ":1063",
70-
rootPrefix: "/",
71-
logLevel: slog.LevelError,
71+
name: "log level warn",
72+
args: []string{"-configPath", "/path/to/config.yaml", "-logLevel", "warn"},
73+
configPath: "/path/to/config.yaml",
74+
addr: ":1063",
75+
rootPrefix: "/",
76+
logLevel: slog.LevelWarn,
77+
enableRedaction: false,
78+
},
79+
{
80+
name: "log level error",
81+
args: []string{"-configPath", "/path/to/config.yaml", "-logLevel", "error"},
82+
configPath: "/path/to/config.yaml",
83+
addr: ":1063",
84+
rootPrefix: "/",
85+
logLevel: slog.LevelError,
86+
enableRedaction: false,
7287
},
7388
{
7489
name: "all extProcFlags",
@@ -78,64 +93,69 @@ func Test_parseAndValidateFlags(t *testing.T) {
7893
"-logLevel", "debug",
7994
"-rootPrefix", "/foo/bar/",
8095
},
81-
configPath: "/path/to/config.yaml",
82-
addr: "unix:///tmp/ext_proc.sock",
83-
rootPrefix: "/foo/bar/",
84-
logLevel: slog.LevelDebug,
96+
configPath: "/path/to/config.yaml",
97+
addr: "unix:///tmp/ext_proc.sock",
98+
rootPrefix: "/foo/bar/",
99+
logLevel: slog.LevelDebug,
100+
enableRedaction: false,
85101
},
86102
{
87-
name: "with endpoint prefixes",
88-
args: []string{"-configPath", "/path/to/config.yaml", "-endpointPrefixes", "openai:/,cohere:/cohere,anthropic:/anthropic"},
89-
configPath: "/path/to/config.yaml",
90-
addr: ":1063",
91-
rootPrefix: "/",
92-
logLevel: slog.LevelInfo,
103+
name: "with endpoint prefixes",
104+
args: []string{"-configPath", "/path/to/config.yaml", "-endpointPrefixes", "openai:/,cohere:/cohere,anthropic:/anthropic"},
105+
configPath: "/path/to/config.yaml",
106+
addr: ":1063",
107+
rootPrefix: "/",
108+
logLevel: slog.LevelInfo,
109+
enableRedaction: false,
93110
},
94111
{
95112
name: "with metrics header mapping",
96113
args: []string{
97114
"-configPath", "/path/to/config.yaml",
98115
"-metricsRequestHeaderAttributes", "x-tenant-id:tenant.id,x-tenant-id:tenant.id",
99116
},
100-
configPath: "/path/to/config.yaml",
101-
rootPrefix: "/",
102-
addr: ":1063",
103-
logLevel: slog.LevelInfo,
117+
configPath: "/path/to/config.yaml",
118+
rootPrefix: "/",
119+
addr: ":1063",
120+
logLevel: slog.LevelInfo,
121+
enableRedaction: false,
104122
},
105123
{
106124
name: "with base header mapping",
107125
args: []string{
108126
"-configPath", "/path/to/config.yaml",
109-
"-requestHeaderAttributes", "x-tenant-id:tenant.id",
127+
"-metricsRequestHeaderAttributes", "x-team-id:team.id,x-user-id:user.id",
110128
},
111-
configPath: "/path/to/config.yaml",
112-
rootPrefix: "/",
113-
addr: ":1063",
114-
logLevel: slog.LevelInfo,
129+
configPath: "/path/to/config.yaml",
130+
rootPrefix: "/",
131+
addr: ":1063",
132+
logLevel: slog.LevelInfo,
133+
enableRedaction: false,
115134
},
116135
{
117-
name: "with tracing and access log header attributes",
136+
name: "with tracing header attributes",
118137
args: []string{
119138
"-configPath", "/path/to/config.yaml",
120-
"-spanRequestHeaderAttributes", "x-forwarded-proto:url.scheme",
121-
"-logRequestHeaderAttributes", "x-forwarded-proto:url.scheme",
139+
"-spanRequestHeaderAttributes", "x-session-id:session.id,x-user-id:user.id",
122140
},
123-
configPath: "/path/to/config.yaml",
124-
rootPrefix: "/",
125-
addr: ":1063",
126-
logLevel: slog.LevelInfo,
141+
configPath: "/path/to/config.yaml",
142+
rootPrefix: "/",
143+
addr: ":1063",
144+
logLevel: slog.LevelInfo,
145+
enableRedaction: false,
127146
},
128147
{
129148
name: "with both metrics and tracing headers",
130149
args: []string{
131150
"-configPath", "/path/to/config.yaml",
132-
"-spanRequestHeaderAttributes", "x-forwarded-proto:url.scheme",
133-
"-metricsRequestHeaderAttributes", "x-tenant-id:tenant.id",
151+
"-metricsRequestHeaderAttributes", "x-user-id:user.id",
152+
"-spanRequestHeaderAttributes", "x-session-id:session.id",
134153
},
135-
configPath: "/path/to/config.yaml",
136-
rootPrefix: "/",
137-
addr: ":1063",
138-
logLevel: slog.LevelInfo,
154+
configPath: "/path/to/config.yaml",
155+
rootPrefix: "/",
156+
addr: ":1063",
157+
logLevel: slog.LevelInfo,
158+
enableRedaction: false,
139159
},
140160
} {
141161
t.Run(tc.name, func(t *testing.T) {
@@ -144,28 +164,12 @@ func Test_parseAndValidateFlags(t *testing.T) {
144164
require.Equal(t, tc.configPath, flags.configPath)
145165
require.Equal(t, tc.addr, flags.extProcAddr)
146166
require.Equal(t, tc.logLevel, flags.logLevel)
167+
require.Equal(t, tc.enableRedaction, flags.enableRedaction)
147168
require.Equal(t, tc.rootPrefix, flags.rootPrefix)
148-
if tc.name == "minimal extProcFlags" {
149-
require.Nil(t, flags.spanRequestHeaderAttributes)
150-
require.Nil(t, flags.logRequestHeaderAttributes)
151-
}
152169
})
153170
}
154171
})
155172

156-
t.Run("empty span/log header mappings clear defaults", func(t *testing.T) {
157-
flags, err := parseAndValidateFlags([]string{
158-
"-configPath", "/path/to/config.yaml",
159-
"-spanRequestHeaderAttributes", "",
160-
"-logRequestHeaderAttributes", "",
161-
})
162-
require.NoError(t, err)
163-
require.NotNil(t, flags.spanRequestHeaderAttributes)
164-
require.NotNil(t, flags.logRequestHeaderAttributes)
165-
require.Empty(t, *flags.spanRequestHeaderAttributes)
166-
require.Empty(t, *flags.logRequestHeaderAttributes)
167-
})
168-
169173
t.Run("invalid extProcFlags", func(t *testing.T) {
170174
tests := []struct {
171175
name string
@@ -189,29 +193,14 @@ func Test_parseAndValidateFlags(t *testing.T) {
189193
},
190194
{
191195
name: "invalid tracing header attributes - missing colon",
192-
args: []string{"-configPath", "/path/to/config.yaml", "-spanRequestHeaderAttributes", "agent-session-id"},
193-
expectedError: "failed to parse tracing header mapping: invalid header-attribute pair at position 1: \"agent-session-id\" (expected format: header:attribute)",
194-
},
195-
{
196-
name: "invalid request header attributes - missing colon",
197-
args: []string{"-configPath", "/path/to/config.yaml", "-requestHeaderAttributes", "agent-session-id"},
198-
expectedError: "failed to parse request header mapping: invalid header-attribute pair at position 1: \"agent-session-id\" (expected format: header:attribute)",
196+
args: []string{"-configPath", "/path/to/config.yaml", "-spanRequestHeaderAttributes", "x-session-id"},
197+
expectedError: "failed to parse tracing header mapping: invalid header-attribute pair at position 1: \"x-session-id\" (expected format: header:attribute)",
199198
},
200199
{
201200
name: "invalid tracing header attributes - empty header",
202201
args: []string{"-configPath", "/path/to/config.yaml", "-spanRequestHeaderAttributes", ":session.id"},
203202
expectedError: "failed to parse tracing header mapping: empty header or attribute at position 1: \":session.id\"",
204203
},
205-
{
206-
name: "invalid access log header attributes - missing colon",
207-
args: []string{"-configPath", "/path/to/config.yaml", "-logRequestHeaderAttributes", "agent-session-id"},
208-
expectedError: "failed to parse access log header mapping: invalid header-attribute pair at position 1: \"agent-session-id\" (expected format: header:attribute)",
209-
},
210-
{
211-
name: "invalid access log header attributes - empty header",
212-
args: []string{"-configPath", "/path/to/config.yaml", "-logRequestHeaderAttributes", ":session.id"},
213-
expectedError: "failed to parse access log header mapping: empty header or attribute at position 1: \":session.id\"",
214-
},
215204
}
216205

217206
for _, tt := range tests {
@@ -233,9 +222,9 @@ func TestListenAddress(t *testing.T) {
233222
defer lis.Close() //nolint:errcheck
234223

235224
tests := []struct {
236-
addr string
237-
expectedNetwork string
238-
expectedAddress string
225+
addr string
226+
wantNetwork string
227+
wantAddress string
239228
}{
240229
{lis.Addr().String(), "tcp", lis.Addr().String()},
241230
{"unix://" + unixPath, "unix", unixPath},
@@ -244,8 +233,8 @@ func TestListenAddress(t *testing.T) {
244233
for _, tt := range tests {
245234
t.Run(tt.addr, func(t *testing.T) {
246235
network, address := listenAddress(tt.addr)
247-
require.Equal(t, tt.expectedNetwork, network)
248-
require.Equal(t, tt.expectedAddress, address)
236+
require.Equal(t, tt.wantNetwork, network)
237+
require.Equal(t, tt.wantAddress, address)
249238
})
250239
}
251240
_, err = os.Stat(unixPath)

internal/controller/controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ var Scheme = runtime.NewScheme()
6262
type Options struct {
6363
// ExtProcLogLevel is the log level for the external processor, e.g., debug, info, warn, or error.
6464
ExtProcLogLevel string
65+
// ExtProcEnableRedaction enables redaction of sensitive information in debug logs for the external processor.
66+
ExtProcEnableRedaction bool
6567
// ExtProcImage is the image for the external processor set on Deployment.
6668
ExtProcImage string
6769
// ExtProcImagePullPolicy is the image pull policy for the external processor set on Deployment.
@@ -244,6 +246,7 @@ func StartControllers(ctx context.Context, mgr manager.Manager, config *rest.Con
244246
options.ExtProcImage,
245247
options.ExtProcImagePullPolicy,
246248
options.ExtProcLogLevel,
249+
options.ExtProcEnableRedaction,
247250
options.UDSPath,
248251
options.RequestHeaderAttributes,
249252
options.TracingRequestHeaderAttributes,

0 commit comments

Comments
 (0)