waf: materialize managed rules (OWASP CRS) as the project Coraza zone#35
Draft
acoshift wants to merge 2 commits into
Draft
waf: materialize managed rules (OWASP CRS) as the project Coraza zone#35acoshift wants to merge 2 commits into
acoshift wants to merge 2 commits into
Conversation
…a zone Generate the deterministic SecLang document from WAFManagedRules (engine mode, explicit paranoia/threshold setvars, the resolving CRS include forms, sorted SecRuleRemoveById exclusions), upsert it as the coraza-<pid> ConfigMap beside the WAF/ratelimit zones, and stamp the parapet.moonrhythm.io/coraza-zone annotation on all project ingresses — both on zone set (syncZoneAnnotations) and on later ingress creation (CreateIngress + CreateRedirectIngress lookups), so new routes stay covered. Disabled/nil managed rules delete the ConfigMap and strip the annotation; WAF zone delete tears the Coraza zone down too. An empty CorazaZoneID in the command means a pre-managed-rules apiserver — all Coraza state is left untouched (the RateLimitZoneID mixed-version pattern). Tests: conf goldens + determinism in k8s/waf_test.go, and in k8s/coraza_test.go the CI compile gate (every golden must compile via the production engine, parapet-ingress-controller/corazawaf over the embedded coreruleset.FS) plus the GET-XSS behavior canary (enforce 403 / clean 200 / detect 200), which pins the unconditional-phase-2 engine contract. Pins: github.com/deploys-app/api @ d27048143494 (waf-managed-rules branch — re-pin to the next api tag after the api PR merges); github.com/moonrhythm/parapet-ingress-controller @ ad90a1a7b02c (PR #181 coraza-v1-fixes — re-pin to the release once cut; carries coraza v3.7.0 + coraza-coreruleset v4.25.0, the same versions the deployed parapet image will pin, so the CI compile is the production compile).
Widen Client.client from *kubernetes.Clientset to kubernetes.Interface so tests can inject k8s.io/client-go/kubernetes/fake, and add a table test over the CreateWAFZone/DeleteWAFZone Coraza branches: materialize (ConfigMap upsert + annotation stamp), disable/nil teardown (ConfigMap delete + annotation strip), delete, and the load-bearing mixed-version guard — an empty CorazaZoneID must leave live Coraza state untouched on both paths, since a regression that strips the annotation on pre-managed-rules commands would silently unbind live zones. Also pins the materialized ConfigMap shape (crs.conf key, generated bytes, the parapet.moonrhythm.io/coraza=zone + projectId labels the controller watch selects on).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Deployer leg of SPEC-waf-managed-rules.md (§4): materialize
WAFManagedRulesas the project's parapet Coraza zone.Do-not-merge gate (why this PR is a draft)
Re-pin
go.modbefore merge — this is the first thing to do, not a follow-up. Both new pins are currently PR-head pseudo-versions that become dangling refs after squash-merge:github.com/deploys-app/api@d27048143494(waf-managed-rules branch) → re-pin to the post-merge api main commit/tag.github.com/moonrhythm/parapet-ingress-controller@v0.15.4-0.20260710175536-ad90a1a7b02c(= PR #181coraza-v1-fixesHEAD, still OPEN; latest release is v0.15.3, pre-fix) → re-pin to the release tag once cut.The parapet pin is not just hygiene: the compile-gate/canary tests (spec §4.5, §10 step 2) are the mechanical enforcement that this PR cannot ship before the §9a engine release exists. With a PR-head pin CI is green while §9a is unreleased, so the PR stays draft until both re-pins land — that is the merge blocker, not review state.
Parapet §9a released and rolled out with
CORAZA_ENABLED=trueon the waf-flagged clusters (rollout step 0).What it does
k8s/coraza.go—generateCorazaConf: deterministic SecLang from the typed knobs (engine mode, explicit paranoia/threshold setvars so the fingerprint is stable across CRS bumps, the resolving include formsInclude @crs-setup.conf.example/Include @owasp_crs/*.conf, sortedSecRuleRemoveByIdexclusions after the includes).k8s/waf.go—CreateWAFZone/DeleteWAFZonegaincorazaZoneID+managed: enabled → upsertcoraza-<pid>ConfigMap (labelparapet.moonrhythm.io/coraza: zone) + stampparapet.moonrhythm.io/coraza-zoneon all project Ingresses via the singlesyncZoneAnnotationspass; nil/disabled → delete ConfigMap + strip annotation. EmptyCorazaZoneID(pre-managed-rules apiserver) leaves all Coraza state untouched — theRateLimitZoneIDmixed-version pattern.k8s/ingress.go—CreateIngressandCreateRedirectIngressadd the fourth best-effort zone lookup (corazaZoneForProject), so routes created after the zone exists stay covered (both halves of the re-stamp rule, spec §4.4).main.go— worker passes the two new command fields through; no new command case.Test evidence
gofmtclean;go build ./... && go vet ./... && go test ./...green — 53/53 in 3 packages, including:k8s/waf_test.go): enforce/detect/defaults/sorted-exclusions conf cases + determinism (input slice not mutated).k8s/coraza_test.go,TestGenerateCorazaConfCompiles): every golden compiles via the production engine (parapet-ingress-controller/corazawafover the embeddedcoreruleset.FS) — a compile rejection in production is controller-log-only and last-good for a new zone is pass-through, so this is a CI invariant.TestCorazaCanaryGETXSS):GET /?q=<script>alert(1)</script>→ 403 enforce / 200 clean / 200 detect — pins the §9a unconditional-phase-2 contract (red on any pre-§9a engine).k8s/waf_zone_test.go, fake clientset): materialize / disable / nil-teardown / delete, ConfigMap shape + labels, and the mixed-version guard on both Create and Delete paths (emptyCorazaZoneIDmust leave a live zone's ConfigMap and annotation untouched).Rollout (spec §10 / §14 — order is load-bearing)
CORAZA_ENABLED=true(+ decideCORAZA_REQUEST_BODY_LIMIT, recommended 131072) on gke.cluster-rcf2 and cluster-lab — before everything. Without the phase-2 fix, CRS never blocks GET query-string attacks.Successwith managed rules silently not materialized (spec §4.6). The new deployer tolerates old apiservers via the empty-CorazaZoneIDguard.waf_zones.managed_rulescolumn migration, then the binary; then flip the location feature flags (ops SQL, by name).Runbook: rollback ⇒ stale enforcement (spec §4.6)
The empty-
CorazaZoneIDguard is deliberately conservative ("leave Coraza state untouched"), which is safe pre-materialization but has a sharp edge once zones are live: during an apiserver rollback past the managed-rules version, a tenant disabling managed rules (or deleting the WAF zone) getsSuccesswhile the stalecoraza-<pid>ConfigMap keeps blocking their traffic. The same edge exists during a deployer binary rollback — an old deployer ignoresCorazaZoneID/ManagedRulesentirely, so a disable in that window also leaves the blocking ConfigMap live. Runbook rule: after any such rollback window with live zones, sweep on roll-forward — re-waf.setaffected zones or delete strayparapet.moonrhythm.io/coraza=zoneConfigMaps whose zone row says disabled; treat disable requests during the window as not yet honored.Expected conflicts with sibling WAF branches
Three sibling branches are open (api/apiserver:
waf-test,waf-ip-lists,waf-events), all cut from origin/main. None of them touch this repo orapi/deployer.go, so no deployer-side conflicts are expected. The one coupling is the apigo.modre-pin: whichever api PRs merge first, the re-pin here must target an api main commit that contains the managed-rules api leg (WAFManagedRules,DeployerCommandWAFSet.CorazaZoneID/ManagedRules); if the sibling api PRs merge before it, the re-pin simply lands on a later main commit that includes all of them — resolution is "pin newest main", no code changes here.