Skip to content

fix: reject inverted TrafficProtectionPolicy paranoia levels at admission#251

Merged
ecv merged 1 commit into
mainfrom
fix/250-paranoia-cel-validation
Jul 11, 2026
Merged

fix: reject inverted TrafficProtectionPolicy paranoia levels at admission#251
ecv merged 1 commit into
mainfrom
fix/250-paranoia-cel-validation

Conversation

@ecv

@ecv ecv commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #250 via option 1 (CEL admission validation) from the issue's root-cause comment.

A TrafficProtectionPolicy whose OWASP CRS blocking paranoia level exceeds its detection paranoia level is an illegal CRS configuration. CRS rule 901500 fails closed and denies every request at phase 1 with HTTP 500 before any attack evaluation, so a misconfigured policy silently 500s 100% of traffic to the protected route. Because blocking and detection were validated independently (each 1..4, default 1), setting blocking: 2 while leaving detection at its default 1 was admissible.

Change

  • Add a CEL XValidation rule on ParanoiaLevels requiring self.detection >= self.blocking, so the illegal combination is rejected at write time (fail-closed, no runtime surprise).
  • Regenerate CRD and API reference docs.

Test

  • e2e regression test/e2e/trafficprotectionpolicy-paranoia-validation/ asserts admission:
    • rejects blocking: 2 / detection: 1
    • rejects blocking: 2 with detection defaulted
    • accepts detection >= blocking

CEL not live-tested locally (kind clusters were stopped); chainsaw test guards CI.

Companion PR

This is the preferred fail-closed fix. Companion #252 implements option 3 (controller status condition) as defense-in-depth for already-persisted policies CEL cannot re-validate. The two are complementary and can merge independently.

A TrafficProtectionPolicy whose blocking paranoia level exceeds its
detection paranoia level is an illegal OWASP CRS configuration. CRS rule
901500 fails closed and denies every request at phase 1 with HTTP 500
before any attack evaluation, so a misconfigured policy silently 500s
100% of traffic to the protected route. Because blocking and detection
were validated independently (each 1..4, default 1), setting blocking: 2
while leaving detection at its default 1 was admissible.

Add a CEL admission rule on ParanoiaLevels requiring detection >=
blocking so the illegal combination is rejected at write time.

Key changes:
- add XValidation rule "self.detection >= self.blocking" on ParanoiaLevels
- regenerate CRD and API reference docs
- add e2e regression asserting admission rejects blocking > detection
@ecv
ecv marked this pull request as ready for review July 11, 2026 01:43
@ecv
ecv merged commit 2ac45c6 into main Jul 11, 2026
10 of 11 checks passed
@ecv
ecv deleted the fix/250-paranoia-cel-validation branch July 11, 2026 01:43
ecv added a commit that referenced this pull request Jul 12, 2026
Add an envtest suite that installs the generated CRDs against a real
apiserver and creates a defaulted TrafficProtectionPolicy. The apiserver
validates a schema node's structural default against that node's own CEL
rules at CRD registration, so an invalid default is rejected during envtest
start — the fake client the rest of the suite uses never sees schema,
defaults, or CEL, which is why the #257 regression went undetected.

The suite skips when KUBEBUILDER_ASSETS is unset (plain go test) and runs
under `make test`, which provisions the assets.

Key checks:
- TPP CRD installs and a policy created with only its required targetRef
  defaults paranoiaLevels to a pair satisfying detection>=blocking.
- An inverted user-supplied pair is still rejected, so the default fix did
  not loosen the #251 rule it depends on.

Part of #257.
ecv added a commit that referenced this pull request Jul 13, 2026
The ParanoiaLevels struct gained an XValidation rule
(self.detection >= self.blocking) in #251, but the field kept an empty
default. The apiserver validates a schema node's structural default against
its own CEL rules, and the default {} has no detection/blocking keys — so
CRD registration fails with "no such key: detection" and kubectl apply of
the CRD is rejected, breaking every branch's e2e "Prepare e2e" step.

Set the field default to {blocking:1,detection:1} so it satisfies the rule
(1 >= 1). Per-field defaults are unchanged and #251's rule still rejects
inverted user-supplied values. CRD manifest regenerated.

Closes #257
ecv added a commit that referenced this pull request Jul 13, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TrafficProtectionPolicy: blocking paranoia level > detection triggers CRS 901500 → 100% of traffic gets HTTP 500

2 participants