From 6eb57eb69023440cd082a2d41103897443e01ab6 Mon Sep 17 00:00:00 2001 From: Evan Vetere Date: Mon, 13 Jul 2026 18:50:13 -0400 Subject: [PATCH 1/3] test: add neutralization e2e for persisted inverted TPP Add a quarantined, expected-red e2e case asserting the controller withholds an already-persisted inverted-paranoia TrafficProtectionPolicy from the coraza edge data plane, so benign traffic still serves 200 instead of the CRS 901500 fail-closed 500. The test injects a grandfathered inverted policy by bypassing the #251 CEL admission rule (patch the paranoiaLevels validation off the CRD, create the policy, restore the rule), then asserts a benign GET returns 200. It fails until #252's skip-attachment is enforced on the downstream-gateway path, so it lives under test/e2e-nonblocking and is excluded from the gating test-e2e run via the separate test-e2e-nonblocking target. The controller-side fix that makes this pass is to follow on this branch. --- Makefile | 8 + .../chainsaw-test.yaml | 236 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 test/e2e-nonblocking/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml diff --git a/Makefile b/Makefile index 64301f0f..8b0cfcf8 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,14 @@ test-e2e: chainsaw --cluster nso-standard=$(TMPDIR)/.kind-nso-standard.yaml \ --cluster nso-infra=$(TMPDIR)/.kind-nso-infra.yaml +.PHONY: test-e2e-nonblocking +test-e2e-nonblocking: chainsaw ## Run quarantined, expected-red e2e tests (NOT part of the gating test-e2e run). + $(KIND) get kubeconfig --name nso-standard > $(TMPDIR)/.kind-nso-standard.yaml + $(KIND) get kubeconfig --name nso-infra > $(TMPDIR)/.kind-nso-infra.yaml + $(CHAINSAW) test ./test/e2e-nonblocking \ + --cluster nso-standard=$(TMPDIR)/.kind-nso-standard.yaml \ + --cluster nso-infra=$(TMPDIR)/.kind-nso-infra.yaml + GATEWAY_CONFORMANCE_CLASS ?= gateway-conformance GATEWAY_CONFORMANCE_FLAGS ?= diff --git a/test/e2e-nonblocking/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml b/test/e2e-nonblocking/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml new file mode 100644 index 00000000..c3626741 --- /dev/null +++ b/test/e2e-nonblocking/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml @@ -0,0 +1,236 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: trafficprotectionpolicy-neutralize-inverted +# +# QUARANTINED — EXPECTED RED until network-services-operator#252 fixes edge-path +# neutralization. This test lives in test/e2e-nonblocking/, which the gating +# `make test-e2e` (chainsaw test ./test/e2e) does NOT discover. Run it on purpose +# with `make test-e2e-nonblocking`. It must never be moved under test/e2e/ while +# it is red, or it will re-red main's E2E gate. +# +# TODO(network-services-operator#252, infra#3408): once the controller refuses to +# enforce an inverted policy on the Coraza edge path, move this test under +# test/e2e/ (into the gate) and keep the benign GET / -> 200 assertion. +# +# What this proves (network-services-operator#242 / #251 / #252): +# Mechanism A is an inverted OWASP policy (blocking paranoia > detection). CRS +# rule 901500 then fails closed and returns HTTP 500 on 100% of traffic. #251 +# added a CRD CEL rule (self.detection >= self.blocking) that REJECTS inverted +# writes at admission, and #252 was meant to neutralize already-persisted +# inverted policies (Accepted=False + skip attachment). #252 does NOT neutralize +# on the real Coraza edge data plane, so a grandfathered inverted policy still +# 500s live. This test injects such a grandfathered policy and asserts a benign +# GET / returns 200 (the desired neutralized behavior). It FAILS today: the +# benign request gets a 500. That failure documents the outstanding defect. +# +# Admission bypass (Case 2 injection): +# #251's CEL rule blocks `kubectl apply` of an inverted TrafficProtectionPolicy, +# so we cannot create one directly. To reproduce a policy that was persisted +# before the CEL rule existed (a grandfathered object), the setup step strips the +# x-kubernetes-validations block from the CRD's paranoiaLevels schema, creates the +# inverted {detection:1, blocking:2} policy while validation is absent, then +# restores the CRD validation. The apiserver does not re-validate existing +# objects on CRD update, so the inverted object survives exactly like a +# grandfathered one, and subsequent writes are rejected again. The strip/restore +# is deterministic: the CRD has a single version (index 0) and one fixed JSON +# path to the validation block. +# +# Precondition: the downstream (nso-infra) must have the WAF data plane wired up +# (`make prepare-infra-cluster` -> `make downstream-waf-dataplane`). +spec: + cluster: nso-infra + # EG only reconciles namespaces carrying this label. + namespaceTemplate: + metadata: + labels: + meta.datumapis.com/upstream-cluster-name: e2e + steps: + - name: Deploy a backend + try: + - apply: + resource: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: echo + spec: + replicas: 1 + selector: + matchLabels: + app: echo + template: + metadata: + labels: + app: echo + spec: + containers: + - name: echo + image: hashicorp/http-echo:1.0 + args: ["-text=hello from backend", "-listen=:8080"] + ports: + - containerPort: 8080 + - apply: + resource: + apiVersion: v1 + kind: Service + metadata: + name: echo + spec: + selector: + app: echo + ports: + - port: 80 + targetPort: 8080 + - assert: + resource: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: echo + status: + availableReplicas: 1 + + - name: Route through the WAF gateway + bindings: + - name: hostname + value: (join('.', [$namespace, 'e2e.test'])) + try: + - apply: + resource: + apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: waf-gw + spec: + gatewayClassName: datum-downstream-gateway + listeners: + - name: http + protocol: HTTP + port: 80 + hostname: ($hostname) + allowedRoutes: + namespaces: + from: Same + - apply: + resource: + apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: echo + spec: + parentRefs: + - name: waf-gw + hostnames: + - ($hostname) + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: echo + port: 80 + + - name: Inject a grandfathered inverted policy by bypassing admission + description: > + Strip the CRD's paranoiaLevels CEL validation, create the inverted + {detection:1, blocking:2} policy, then restore the validation so the + object survives like a grandfathered one and future writes are rejected + again. + try: + - script: + timeout: 120s + content: | + set -eu + crd=trafficprotectionpolicies.networking.datumapis.com + path=/spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/ruleSets/items/properties/owaspCoreRuleSet/properties/paranoiaLevels/x-kubernetes-validations + + kubectl patch crd "${crd}" --type=json \ + -p "[{\"op\":\"remove\",\"path\":\"${path}\"}]" + + restore() { + kubectl patch crd "${crd}" --type=json -p "[{\"op\":\"add\",\"path\":\"${path}\",\"value\":[{\"message\":\"detection paranoia level must be greater than or equal to blocking paranoia level\",\"rule\":\"self.detection >= self.blocking\"}]}]" + } + trap restore EXIT + + created=false + for i in $(seq 1 20); do + if cat <<'EOF' | kubectl apply -f - + apiVersion: networking.datumapis.com/v1alpha + kind: TrafficProtectionPolicy + metadata: + name: inverted-waf + spec: + mode: Enforce + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: waf-gw + ruleSets: + - type: OWASPCoreRuleSet + owaspCoreRuleSet: + paranoiaLevels: + detection: 1 + blocking: 2 + EOF + then + created=true + break + fi + echo "apply attempt ${i} failed (schema cache lag?), retrying" + sleep 2 + done + + if [ "${created}" != "true" ]; then + echo "failed to inject inverted policy while CEL was stripped" + exit 1 + fi + + blocking=$(kubectl get trafficprotectionpolicy inverted-waf \ + -o jsonpath='{.spec.ruleSets[0].owaspCoreRuleSet.paranoiaLevels.blocking}') + detection=$(kubectl get trafficprotectionpolicy inverted-waf \ + -o jsonpath='{.spec.ruleSets[0].owaspCoreRuleSet.paranoiaLevels.detection}') + echo "injected inverted policy: blocking=${blocking} detection=${detection}" + if [ "${blocking}" != "2" ] || [ "${detection}" != "1" ]; then + echo "inverted policy not persisted as expected" + exit 1 + fi + + - name: A benign GET must return 200, not 500 + description: > + The neutralized/desired behavior. FAILS today: because #252 does not + neutralize on the Coraza edge path, the inverted policy makes CRS rule + 901500 fail closed and every request 500s. Un-skip (move under test/e2e/) + once #252 lands. + bindings: + - name: hostname + value: (join('.', [$namespace, 'e2e.test'])) + try: + - script: + env: + - name: HOSTNAME + value: ($hostname) + content: | + set -u + for i in $(seq 1 40); do + code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \ + -H "Host: ${HOSTNAME}" http://localhost:30080/) || code=000 + echo "attempt ${i}: HTTP ${code}" + if [ "${code}" = "200" ]; then + exit 0 + fi + if [ "${code}" = "500" ]; then + echo "inverted policy still enforced on the edge (500) — #252 defect" + fi + sleep 3 + done + echo "benign request never returned 200 (network-services-operator#252 defect)" + exit 1 + finally: + - delete: + ref: + apiVersion: networking.datumapis.com/v1alpha + kind: TrafficProtectionPolicy + name: inverted-waf From d0ca80ade676c02de39ff52355eb05fe09589729 Mon Sep 17 00:00:00 2001 From: Evan Vetere Date: Mon, 13 Jul 2026 19:02:19 -0400 Subject: [PATCH 2/3] fix: withhold inverted TPP from coraza edge path An inverted-paranoia TrafficProtectionPolicy (OWASP CRS detection below blocking) makes CRS rule 901500 fail closed and 500 every request. #252 set Accepted=False/Invalid and skipped attachment on the controller's EnvoyPatchPolicy path, but the live edge enforces coraza through the extension server, which builds per-route directives in a separate path (internal/extensionserver/cache) that #252 never touched. A grandfathered inverted policy therefore still 500'd benign traffic on the downstream gateway. Gate the extension server's directive emission on the same inverted-level check the controller uses, so an inverted policy produces no directives and the mutation layer withholds it from the data plane. A benign request is then unaffected instead of 500'ing. Key changes: - add shared TrafficProtectionPolicySpec.InvertedParanoiaLevels predicate and route both the controller resolve-error guard and the extension server emission through it - return nil directives for an inverted policy in computeCorazaDirectives so ApplyTPPRouteConfig skips the route - add API and emission-layer unit tests: inverted not emitted, valid and equal levels emitted - promote the neutralization e2e into the gating test/e2e suite and drop the now-empty test-e2e-nonblocking target Fixes #273. Refs #252, #242, #268. --- Makefile | 8 -- api/v1alpha/trafficprotectionpolicy_types.go | 13 ++++ .../trafficprotectionpolicy_types_test.go | 77 +++++++++++++++++++ .../trafficprotectionpolicy_controller.go | 23 +++--- internal/extensionserver/cache/index.go | 4 + internal/extensionserver/cache/index_test.go | 57 ++++++++++++++ .../chainsaw-test.yaml | 35 +++------ 7 files changed, 172 insertions(+), 45 deletions(-) create mode 100644 api/v1alpha/trafficprotectionpolicy_types_test.go rename test/{e2e-nonblocking => e2e}/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml (84%) diff --git a/Makefile b/Makefile index 8b0cfcf8..64301f0f 100644 --- a/Makefile +++ b/Makefile @@ -85,14 +85,6 @@ test-e2e: chainsaw --cluster nso-standard=$(TMPDIR)/.kind-nso-standard.yaml \ --cluster nso-infra=$(TMPDIR)/.kind-nso-infra.yaml -.PHONY: test-e2e-nonblocking -test-e2e-nonblocking: chainsaw ## Run quarantined, expected-red e2e tests (NOT part of the gating test-e2e run). - $(KIND) get kubeconfig --name nso-standard > $(TMPDIR)/.kind-nso-standard.yaml - $(KIND) get kubeconfig --name nso-infra > $(TMPDIR)/.kind-nso-infra.yaml - $(CHAINSAW) test ./test/e2e-nonblocking \ - --cluster nso-standard=$(TMPDIR)/.kind-nso-standard.yaml \ - --cluster nso-infra=$(TMPDIR)/.kind-nso-infra.yaml - GATEWAY_CONFORMANCE_CLASS ?= gateway-conformance GATEWAY_CONFORMANCE_FLAGS ?= diff --git a/api/v1alpha/trafficprotectionpolicy_types.go b/api/v1alpha/trafficprotectionpolicy_types.go index fc0350a1..d1ea3faf 100644 --- a/api/v1alpha/trafficprotectionpolicy_types.go +++ b/api/v1alpha/trafficprotectionpolicy_types.go @@ -129,6 +129,19 @@ type ParanoiaLevels struct { Detection int `json:"detection,omitempty"` } +func (s *TrafficProtectionPolicySpec) InvertedParanoiaLevels() *ParanoiaLevels { + for i := range s.RuleSets { + if s.RuleSets[i].Type != TrafficProtectionPolicyOWASPCoreRuleSet { + continue + } + levels := &s.RuleSets[i].OWASPCoreRuleSet.ParanoiaLevels + if levels.Detection < levels.Blocking { + return levels + } + } + return nil +} + type OWASPRuleExclusions struct { // Tags is a list of rule tags to disable. // diff --git a/api/v1alpha/trafficprotectionpolicy_types_test.go b/api/v1alpha/trafficprotectionpolicy_types_test.go new file mode 100644 index 00000000..d89cb6bd --- /dev/null +++ b/api/v1alpha/trafficprotectionpolicy_types_test.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha" +) + +func TestTrafficProtectionPolicySpec_InvertedParanoiaLevels(t *testing.T) { + t.Parallel() + + owaspRuleSet := func(blocking, detection int) networkingv1alpha.TrafficProtectionPolicyRuleSet { + return networkingv1alpha.TrafficProtectionPolicyRuleSet{ + Type: networkingv1alpha.TrafficProtectionPolicyOWASPCoreRuleSet, + OWASPCoreRuleSet: networkingv1alpha.OWASPCRS{ + ParanoiaLevels: networkingv1alpha.ParanoiaLevels{ + Blocking: blocking, + Detection: detection, + }, + }, + } + } + + tests := []struct { + name string + ruleSets []networkingv1alpha.TrafficProtectionPolicyRuleSet + wantInverted bool + }{ + { + name: "no rulesets", + ruleSets: nil, + wantInverted: false, + }, + { + name: "equal levels", + ruleSets: []networkingv1alpha.TrafficProtectionPolicyRuleSet{owaspRuleSet(2, 2)}, + wantInverted: false, + }, + { + name: "higher detection", + ruleSets: []networkingv1alpha.TrafficProtectionPolicyRuleSet{owaspRuleSet(1, 3)}, + wantInverted: false, + }, + { + name: "detection below blocking", + ruleSets: []networkingv1alpha.TrafficProtectionPolicyRuleSet{owaspRuleSet(2, 1)}, + wantInverted: true, + }, + { + name: "detection far below blocking", + ruleSets: []networkingv1alpha.TrafficProtectionPolicyRuleSet{owaspRuleSet(4, 1)}, + wantInverted: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + spec := networkingv1alpha.TrafficProtectionPolicySpec{RuleSets: tt.ruleSets} + levels := spec.InvertedParanoiaLevels() + + if tt.wantInverted { + if assert.NotNil(t, levels, "inverted ruleset must be reported") { + assert.Less(t, levels.Detection, levels.Blocking, + "reported levels must be inverted") + } + return + } + assert.Nil(t, levels, "no ruleset is inverted") + }) + } +} diff --git a/internal/controller/trafficprotectionpolicy_controller.go b/internal/controller/trafficprotectionpolicy_controller.go index f5ee779d..b179bc2c 100644 --- a/internal/controller/trafficprotectionpolicy_controller.go +++ b/internal/controller/trafficprotectionpolicy_controller.go @@ -1235,21 +1235,16 @@ func getVHostConstraintForGateway(namespace string, gateway *gatewayv1.Gateway) // closed on this configuration and denies every request with HTTP 500 before any // attack evaluation, so the policy must not be attached. func paranoiaLevelsResolveError(policy *policyContext) *gatewaystatus.PolicyResolveError { - for _, ruleSet := range policy.Spec.RuleSets { - if ruleSet.Type != networkingv1alpha.TrafficProtectionPolicyOWASPCoreRuleSet { - continue - } - levels := ruleSet.OWASPCoreRuleSet.ParanoiaLevels - if levels.Detection < levels.Blocking { - return &gatewaystatus.PolicyResolveError{ - Reason: gatewayv1.PolicyReasonInvalid, - Message: fmt.Sprintf( - "OWASPCoreRuleSet detection paranoia level (%d) must be greater than or equal to blocking paranoia level (%d); this is an illegal CRS configuration that would deny all traffic with HTTP 500", - levels.Detection, levels.Blocking), - } - } + levels := policy.Spec.InvertedParanoiaLevels() + if levels == nil { + return nil + } + return &gatewaystatus.PolicyResolveError{ + Reason: gatewayv1.PolicyReasonInvalid, + Message: fmt.Sprintf( + "OWASPCoreRuleSet detection paranoia level (%d) must be greater than or equal to blocking paranoia level (%d); this is an illegal CRS configuration that would deny all traffic with HTTP 500", + levels.Detection, levels.Blocking), } - return nil } func (r *TrafficProtectionPolicyReconciler) getCorazaDirectivesForTrafficProtectionPolicy( diff --git a/internal/extensionserver/cache/index.go b/internal/extensionserver/cache/index.go index 1ab53da1..171a097d 100644 --- a/internal/extensionserver/cache/index.go +++ b/internal/extensionserver/cache/index.go @@ -225,6 +225,10 @@ func computeCorazaDirectives( tpp *networkingv1alpha.TrafficProtectionPolicy, baseDirectives []string, ) []string { + if tpp.Spec.InvertedParanoiaLevels() != nil { + return nil + } + var owaspCRS *networkingv1alpha.OWASPCRS for _, ruleSet := range tpp.Spec.RuleSets { if ruleSet.Type == networkingv1alpha.TrafficProtectionPolicyOWASPCoreRuleSet { diff --git a/internal/extensionserver/cache/index_test.go b/internal/extensionserver/cache/index_test.go index ed59a78d..5ce69ea9 100644 --- a/internal/extensionserver/cache/index_test.go +++ b/internal/extensionserver/cache/index_test.go @@ -485,6 +485,33 @@ func TestBuildPolicyIndexFromClient_TPPFieldsPreserved(t *testing.T) { assert.NotEmpty(t, info.Directives, "OWASP CRS rules must generate non-empty directives") } +func TestBuildPolicyIndexFromClient_InvertedParanoia_NoDirectivesEmitted(t *testing.T) { + scheme := indexTestScheme(t) + + inverted := newTPP("test-ns", "inverted-tpp", withOWASPCRS(5, 4, 2, 1)) + inverted.Spec.Mode = networkingv1alpha.TrafficProtectionPolicyEnforce + + valid := newTPP("test-ns", "valid-tpp", withOWASPCRS(5, 4, 1, 1)) + valid.Spec.Mode = networkingv1alpha.TrafficProtectionPolicyEnforce + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(inverted, valid).Build() + + idx, err := BuildPolicyIndexFromClient(context.Background(), cl, nil) + require.NoError(t, err) + + byName := map[string]TPPInfo{} + for _, info := range idx.TPPs["test-ns"] { + byName[info.Name] = info + } + require.Contains(t, byName, "inverted-tpp") + require.Contains(t, byName, "valid-tpp") + + assert.Empty(t, byName["inverted-tpp"].Directives, + "inverted policy must yield no directives so the mutation layer withholds it from the data plane") + assert.NotEmpty(t, byName["valid-tpp"].Directives, + "valid policy must still emit directives") +} + // ============================================================================= // computeCorazaDirectives unit tests // @@ -594,6 +621,36 @@ func TestComputeCorazaDirectives_ParanoiaLevels(t *testing.T) { assert.True(t, foundDetection, "detection paranoia level 4 must appear in directives") } +func TestComputeCorazaDirectives_InvertedParanoiaLevels(t *testing.T) { + tests := []struct { + name string + blocking int + detection int + wantEmitted bool + }{ + {name: "equal levels emitted", blocking: 2, detection: 2, wantEmitted: true}, + {name: "higher detection emitted", blocking: 1, detection: 3, wantEmitted: true}, + {name: "inverted not emitted", blocking: 2, detection: 1, wantEmitted: false}, + {name: "inverted far not emitted", blocking: 4, detection: 1, wantEmitted: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tpp := newTPP("ns", "tpp", withOWASPCRS(5, 4, tt.blocking, tt.detection)) + tpp.Spec.Mode = networkingv1alpha.TrafficProtectionPolicyEnforce + + result := computeCorazaDirectives(tpp, nil) + + if tt.wantEmitted { + assert.NotEmpty(t, result, "valid paranoia levels must emit directives") + return + } + assert.Nil(t, result, + "inverted paranoia levels must emit no directives (withheld from the data plane)") + }) + } +} + func TestComputeCorazaDirectives_IncludeOWASPCRSAppendedAfterActions(t *testing.T) { tpp := newTPP("ns", "tpp", withOWASPCRS(5, 4, 1, 1)) diff --git a/test/e2e-nonblocking/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml b/test/e2e/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml similarity index 84% rename from test/e2e-nonblocking/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml rename to test/e2e/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml index c3626741..c4a6ee6b 100644 --- a/test/e2e-nonblocking/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml +++ b/test/e2e/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml @@ -4,26 +4,16 @@ kind: Test metadata: name: trafficprotectionpolicy-neutralize-inverted # -# QUARANTINED — EXPECTED RED until network-services-operator#252 fixes edge-path -# neutralization. This test lives in test/e2e-nonblocking/, which the gating -# `make test-e2e` (chainsaw test ./test/e2e) does NOT discover. Run it on purpose -# with `make test-e2e-nonblocking`. It must never be moved under test/e2e/ while -# it is red, or it will re-red main's E2E gate. -# -# TODO(network-services-operator#252, infra#3408): once the controller refuses to -# enforce an inverted policy on the Coraza edge path, move this test under -# test/e2e/ (into the gate) and keep the benign GET / -> 200 assertion. -# -# What this proves (network-services-operator#242 / #251 / #252): +# What this proves (network-services-operator#242 / #251 / #252 / #273): # Mechanism A is an inverted OWASP policy (blocking paranoia > detection). CRS # rule 901500 then fails closed and returns HTTP 500 on 100% of traffic. #251 # added a CRD CEL rule (self.detection >= self.blocking) that REJECTS inverted -# writes at admission, and #252 was meant to neutralize already-persisted -# inverted policies (Accepted=False + skip attachment). #252 does NOT neutralize -# on the real Coraza edge data plane, so a grandfathered inverted policy still -# 500s live. This test injects such a grandfathered policy and asserts a benign -# GET / returns 200 (the desired neutralized behavior). It FAILS today: the -# benign request gets a 500. That failure documents the outstanding defect. +# writes at admission, and #252 set Accepted=False + skipped attachment on the +# EnvoyPatchPolicy path — but that did NOT neutralize the real Coraza edge data +# plane, where the extension server still emitted the inverted policy's +# directives (#273). This test injects a grandfathered inverted policy and +# asserts a benign GET / returns 200: the extension server now withholds the +# inverted policy's directives, so CRS 901500 never fires. # # Admission bypass (Case 2 injection): # #251's CEL rule blocks `kubectl apply` of an inverted TrafficProtectionPolicy, @@ -200,10 +190,9 @@ spec: - name: A benign GET must return 200, not 500 description: > - The neutralized/desired behavior. FAILS today: because #252 does not - neutralize on the Coraza edge path, the inverted policy makes CRS rule - 901500 fail closed and every request 500s. Un-skip (move under test/e2e/) - once #252 lands. + The neutralized behavior. The extension server withholds the inverted + policy's Coraza directives from the edge data plane, so CRS rule 901500 + never fires and benign traffic is unaffected. bindings: - name: hostname value: (join('.', [$namespace, 'e2e.test'])) @@ -222,11 +211,11 @@ spec: exit 0 fi if [ "${code}" = "500" ]; then - echo "inverted policy still enforced on the edge (500) — #252 defect" + echo "inverted policy still enforced on the edge (500) — regression of #273" fi sleep 3 done - echo "benign request never returned 200 (network-services-operator#252 defect)" + echo "benign request never returned 200 (regression of network-services-operator#273)" exit 1 finally: - delete: From 531e6c4924b16616ecd9bf87cebe34a558c77860 Mon Sep 17 00:00:00 2001 From: Evan Vetere Date: Mon, 13 Jul 2026 19:23:09 -0400 Subject: [PATCH 3/3] test: fix neutralize-inverted e2e probe timeout and readiness The benign-GET probe returned HTTP 000 (connection refused), not the neutralized 200, because two harness defects fired before the WAF path was ever exercised: - the probe script had no timeout, so chainsaw's ~5s default killed the 40-attempt retry loop after 2 attempts; - nothing waited for the freshly-created waf-gw to reach Programmed=True, so the probe hit the cold-start window. Add a 180s script timeout and gate the probe on Gateway Programmed=True (3m, matching the enforce test), so the loop retries into the ready window. The controller fix under test is unchanged. --- .../chainsaw-test.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/e2e/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml b/test/e2e/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml index c4a6ee6b..5482d51f 100644 --- a/test/e2e/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml +++ b/test/e2e/trafficprotectionpolicy-neutralize-inverted/chainsaw-test.yaml @@ -122,6 +122,16 @@ spec: backendRefs: - name: echo port: 80 + - assert: + timeout: 3m + resource: + apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: waf-gw + status: + (conditions[?type == 'Programmed']): + - status: "True" - name: Inject a grandfathered inverted policy by bypassing admission description: > @@ -198,6 +208,7 @@ spec: value: (join('.', [$namespace, 'e2e.test'])) try: - script: + timeout: 180s env: - name: HOSTNAME value: ($hostname)