From 59f46a9b5ad8184f7842910e670f38a8ac32913f Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 5 May 2026 17:44:00 -0500 Subject: [PATCH] fix: target specific key in typed_per_filter_config for coraza patches EnvoyPatchPolicy patches for the Coraza WAF filter were using op:add on /typed_per_filter_config, which replaces the entire map and wipes out per-route filter enablement entries written by other filters (e.g., the oauth2 entry that enables OIDC on a route). Now patches target /typed_per_filter_config/ directly, and the coraza config value no longer wraps itself in the filter name key. Fixes #149 Co-Authored-By: Claude Sonnet 4.6 --- .../trafficprotectionpolicy_controller.go | 34 +++++++++---------- ...trafficprotectionpolicy_controller_test.go | 27 +++++++++++++++ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/internal/controller/trafficprotectionpolicy_controller.go b/internal/controller/trafficprotectionpolicy_controller.go index 9bbe8c8..fdec558 100644 --- a/internal/controller/trafficprotectionpolicy_controller.go +++ b/internal/controller/trafficprotectionpolicy_controller.go @@ -1042,21 +1042,19 @@ func (r *TrafficProtectionPolicyReconciler) getDesiredEnvoyPatchPolicies( } corazaConfig := map[string]any{ - r.Config.Gateway.Coraza.FilterName: map[string]any{ - "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.ConfigsPerRoute", - "plugins_config": map[string]any{ - r.Config.Gateway.Coraza.PluginName: map[string]any{ - "config": map[string]any{ - "@type": "type.googleapis.com/xds.type.v3.TypedStruct", - "value": map[string]any{ - "log_format": "json", - "directives": sanitizeJSONPath(fmt.Sprintf(`{ - "coraza": { - "simple_directives": %s - } - }`, string(directiveBytes))), - "default_directive": "coraza", - }, + "@type": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.ConfigsPerRoute", + "plugins_config": map[string]any{ + r.Config.Gateway.Coraza.PluginName: map[string]any{ + "config": map[string]any{ + "@type": "type.googleapis.com/xds.type.v3.TypedStruct", + "value": map[string]any{ + "log_format": "json", + "directives": sanitizeJSONPath(fmt.Sprintf(`{ + "coraza": { + "simple_directives": %s + } + }`, string(directiveBytes))), + "default_directive": "coraza", }, }, }, @@ -1077,7 +1075,7 @@ func (r *TrafficProtectionPolicyReconciler) getDesiredEnvoyPatchPolicies( Operation: envoygatewayv1alpha1.JSONPatchOperation{ Op: "add", JSONPath: ptr.To(httpRoutesJSONPath), - Path: ptr.To("/typed_per_filter_config"), + Path: ptr.To(fmt.Sprintf("/typed_per_filter_config/%s", r.Config.Gateway.Coraza.FilterName)), Value: &apiextensionsv1.JSON{Raw: corazaConfigBytes}, }, }) @@ -1108,7 +1106,7 @@ func (r *TrafficProtectionPolicyReconciler) getDesiredEnvoyPatchPolicies( Operation: envoygatewayv1alpha1.JSONPatchOperation{ Op: "add", JSONPath: ptr.To(httpRoutesJSONPath), - Path: ptr.To("/typed_per_filter_config"), + Path: ptr.To(fmt.Sprintf("/typed_per_filter_config/%s", r.Config.Gateway.Coraza.FilterName)), Value: &apiextensionsv1.JSON{Raw: corazaConfigBytes}, }, }) @@ -1137,7 +1135,7 @@ func (r *TrafficProtectionPolicyReconciler) getDesiredEnvoyPatchPolicies( Operation: envoygatewayv1alpha1.JSONPatchOperation{ Op: "add", JSONPath: ptr.To(httpRoutesJSONPath), - Path: ptr.To("/typed_per_filter_config"), + Path: ptr.To(fmt.Sprintf("/typed_per_filter_config/%s", r.Config.Gateway.Coraza.FilterName)), Value: &apiextensionsv1.JSON{Raw: corazaConfigBytes}, }, }) diff --git a/internal/controller/trafficprotectionpolicy_controller_test.go b/internal/controller/trafficprotectionpolicy_controller_test.go index 6f67857..c664bfd 100644 --- a/internal/controller/trafficprotectionpolicy_controller_test.go +++ b/internal/controller/trafficprotectionpolicy_controller_test.go @@ -684,6 +684,10 @@ func TestGetDesiredEnvoyPatchPolicies(t *testing.T) { gatewayv1.AnnotationKey("gateway.networking.datumapis.com/certificate-issuer"): gatewayv1.AnnotationValue("test"), }, DownstreamGatewayClassName: "test-gateway-class", + Coraza: config.CorazaConfig{ + FilterName: "coraza-waf", + PluginName: "coraza-waf", + }, }, } @@ -815,6 +819,29 @@ func TestGetDesiredEnvoyPatchPolicies(t *testing.T) { if !assert.Truef(t, patchFound, "did not find patch with vhost constraints %q, listener constraints %q, and route constraints %q", vhostConstraints, listenerConstraint, routeConstraint) { spew.Dump(patchPolicy.Spec.JSONPatches) } + + // Verify that coraza patches target only the specific filter key, not the + // entire typed_per_filter_config map — replacing the whole map would wipe + // out per-route enablement entries written by other filters (e.g., oauth2). + expectedCorazaPath := fmt.Sprintf("/typed_per_filter_config/%s", reconciler.Config.Gateway.Coraza.FilterName) + for _, patch := range patchPolicy.Spec.JSONPatches { + if patch.Name != fmt.Sprintf("http-%d", DefaultHTTPPort) { + continue + } + if !strings.Contains(ptr.Deref(patch.Operation.JSONPath, ""), vhostConstraints) { + continue + } + if ptr.Deref(patch.Operation.Path, "") == expectedCorazaPath { + // found exactly one coraza patch with the right path + break + } + // Any patch whose path starts with /typed_per_filter_config must be the + // coraza one and must target the specific key, not the whole map. + if strings.HasPrefix(ptr.Deref(patch.Operation.Path, ""), "/typed_per_filter_config") { + assert.Equal(t, expectedCorazaPath, ptr.Deref(patch.Operation.Path, ""), + "coraza patch must target /typed_per_filter_config/, not the whole map") + } + } } // Confirm there's a patch for the TLS filter chain for each HTTPS listener