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