Skip to content

waf: managed rules (OWASP CRS) contract — WAFManagedRules, location feature flag, deployer command fields#132

Open
acoshift wants to merge 2 commits into
mainfrom
waf-managed-rules
Open

waf: managed rules (OWASP CRS) contract — WAFManagedRules, location feature flag, deployer command fields#132
acoshift wants to merge 2 commits into
mainfrom
waf-managed-rules

Conversation

@acoshift

Copy link
Copy Markdown
Member

WAF managed rules (OWASP CRS) — API contract

Implements row 1 (api repo) of SPEC-waf-managed-rules.md (§2): the pure contract for managed signature rules on the existing WAF zone. Typed knobs only — the platform generates SecLang downstream (deployer); raw SecLang is never accepted. No new RPC verbs and no new resource.

What's in here

  • WAFManagedRules (waf.go): enabled, mode (enforce/detect), paranoiaLevel (1..4), anomalyThreshold (1..100), excludedRules (CRS detection-rule ids only). WAFSet/WAFItem gain the field; whole-replace semantics — omitted/nil clears the block, enabled:false keeps the tuning stored (spec §2.2). Unset blocks are omitted from waf.get/waf.list JSON (no "managedRules": null).
  • validWAFManagedRules (spec §2.2): structural validation, applied identically to enabled and disabled blocks so garbage tuning can't hide behind enabled:false. Exclusion ids bounded to 911100..948999 — the platform SecActions/CRS setup (≤901xxx) and the anomaly scoring/blocking machinery (949110+) are unreachable by exclusion.
  • Constants (constraint.go): WAFManagedMaxExcludedRules (50), WAFManagedExcludedRuleIDMin/Max.
  • Feature gate (spec §2.3, location.go): LocationFeatures.WAF widens *struct{}*WAFLocationFeature{ManagedRules bool}. JSON-compatible both directions: legacy {"waf":{}} unmarshals to the zero feature, and the zero feature marshals back to {"waf":{}} (both pinned by tests).
  • Deployer command fields (spec §2.4, deployer.go): DeployerCommandWAFSet.CorazaZoneID + .ManagedRules, DeployerCommandWAFDelete.CorazaZoneID. Empty CorazaZoneID = command from a pre-managed-rules apiserver → deployer leaves Coraza state untouched (the RateLimitZoneID mixed-version pattern, spec §4.6).
  • Table output: CRS column (on / off disabled-but-tuned / - unset) in waf.list and waf.get tables.

Breaking change (Go source only)

LocationFeatures.WAF changes type *struct{}*WAFLocationFeature. Wire/JSON is fully compatible; external Go consumers comparing or constructing the old anonymous type must switch to the named type (spec §2.3). Flagging here since the repo has no changelog file.

Test evidence

gofmt clean; go build ./... && go vet ./... && go test ./... green — 70 tests pass across 2 packages. New coverage in waf_managedrules_test.go:

  • TestWAFSetValidManagedRules — valid/invalid matrix incl. bounds by literal (900000/901100/949110 rejected), duplicates, max-exclusions, and disabled-block-still-validated.
  • TestWAFTableCRSColumn — on/off/- three-state cell in both tables.
  • TestWAFManagedRulesWire — pins exact wire keys, omission of unset managedRules/empty excludedRules, and that old-apiserver payloads unmarshal CorazaZoneID to "" and ManagedRules to nil (the deployer's mixed-version guard).
  • TestWAFLocationFeatureCompat — legacy {"waf":{}}, flag-set, absent, and the marshal direction back to {"waf":{}}.

Rollout (spec §10 / §14)

This feature gates on a parapet release: moonrhythm/parapet-ingress-controller#181 (spec §9a — unconditional phase-2 evaluation, zone label on parapet_coraza_matches, resolving CRS include forms + compile test) must be merged, released, and rolled out with CORAZA_ENABLED=true on the waf-flagged clusters before anything materializes. This api PR itself is rollout step 1 and is safe to merge/tag first — it adds contract only.

Order after that: deployer PR (deploy binary before apiserver — spec §4.6 mixed-version matrix) → apiserver PR (migration before binary, then flip features.waf.managedRules per location via ops SQL, values not in PR text) → console/docs/mcp → deploys CLI re-pin gate: the CLI re-pins this api module only after the apiserver is deployed, so waf.set with managedRules never validates client-side against a server that drops it.

Rollback note for downstream PRs: an apiserver rollback past this contract leaves stale Coraza ConfigMaps enforcing (empty-CorazaZoneID = leave untouched); the runbook sweep lives in the apiserver/deployer PR descriptions (spec §4.6).

Expected conflicts with sibling WAF branches

Three open contract branches touch the same files: #129 (waf-test), #130 (waf-ip-lists), #131 (waf-events). All append to the same constraint.go const block and edit waf.go, so whichever lands second gets trivial textual conflicts — intended resolution: keep both hunks (the additions are disjoint). One semantic overlap: #129's WAFTest doc says a zone draft is "the same payload as WAFSet"; once managedRules lands that is no longer exact (the Coraza engine has no CEL dry-run path) — the second-lander should amend that comment to scope waf.test to rules/limits. No semantic overlap with #130/#131.

acoshift added 2 commits July 11, 2026 10:21
…ag, deployer command fields

The api leg of WAF managed rules (SPEC-waf-managed-rules.md §2, §2.4):

- WAFManagedRules (enabled, mode enforce|detect, paranoiaLevel 1-4,
  anomalyThreshold 1-100, excludedRules capped at 50 and bounded to CRS
  detection ids 911100..948999) as managedRules on WAFSet/WAFItem.
  Whole-replace semantics: nil clears; enabled:false keeps tuning.
- validWAFManagedRules structural validation; a disabled block is still
  fully validated so bad tuning can't hide behind a toggle-off.
- CRS table column (on / off / -) between LIMITS and STATUS.
- LocationFeatures.WAF widens *struct{} -> *WAFLocationFeature
  {managedRules bool}; legacy {"waf": {}} stays JSON-compatible.
- DeployerCommandWAFSet gains CorazaZoneID + ManagedRules,
  DeployerCommandWAFDelete gains CorazaZoneID — empty CorazaZoneID means
  a pre-managed-rules apiserver, deployer leaves Coraza state untouched
  (the RateLimitZoneID mixed-version pattern).

Client passthrough needs no code change (fields ride the existing
waf.set/waf.get payloads).
…g marshal shape

Review polish before downstream consumers pin the wire shape:
- WAFItem.ManagedRules and WAFManagedRules.ExcludedRules gain json
  omitempty so unconfigured zones don't emit "managedRules": null and
  stored blocks don't emit "excludedRules": null in waf.get/waf.list.
- WAFItem doc now states the all-zero normalization (spec §3.2):
  managedRules:{} reads back as unset, not as "off".
- Tests: WAFItem omission pinned in TestWAFManagedRulesWire; marshal
  direction of the widened feature flag pinned in
  TestWAFLocationFeatureCompat (zero WAFLocationFeature -> {"waf":{}}).
@acoshift acoshift force-pushed the waf-managed-rules branch from 35a41f9 to 76df2fb Compare July 11, 2026 03:21
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.

1 participant