Recipe 9 — Ban an offender manually: a CEL rule with action: ban

Tool Section Level Time Layer Tags
cmd/engine/ · internal/operator/ Enforcement medium 5 min policy + engine ban, CEL, WAFPolicy, TR-195

Problem

You watched one source hammer a route. You want to stop that source now. You do not want to wait for a score. You want the stop to leave evidence and to name the rule that wrote it.

Solution

A rule pack carries one rule with action: ban. The matcher can say one word: the path. The source stays on the wall as a would-ban record.

  1. Write the rule pack. The action is ban; the match is narrow:
rules:
  - id: OPS-MANUAL-01
    name: manual-ban
    description: stop the offender on the login route
    action: ban
    expression: |
      request.method == "POST" && path.matches("/v1/chat/*")
  1. Ship it the way every rule ships: the ConfigMap + the WAFPolicy CR. The CR points at the pack; Recipe 2 owns that surface. Compile and apply. The gate compiles the pack; the CRD carries the bundle.
kubectl -n trishula apply -f rulepack-manual-ban.yaml
kubectl -n trishula apply -f wafpolicy-with-ban.yaml
  1. Probe the route from the offender source. The response carries the decision, the rule id and the body:
curl -s -i -X POST "http://<gateway>/v1/chat/completions?q=x"
HTTP/1.1 403 Forbidden
X-Trishula-Decision: ban
X-Trishula-Hop: engine
X-Trishula-Rule: OPS-MANUAL-01

banned by OPS-MANUAL-01
  1. Read the ban evidence. Start the engine with --ban-evidence, then read the JSONL line the match wrote:
kubectl -n trishula logs deploy/trishula-engine | tail -3
would-ban 192.0.2.10 tier=manual-rule score=0 sig={Class:manual-rule Paths:[/v1/chat/completions]}
  1. Lift the ban. Remove the rule from the pack and re-apply. The next bundle no longer names the rule, so the match disappears. The scored window for that source decays on its own clock.

Discussion

action: ban folds with the response ladder as a block class on the inside. The same action folds as a direct would-ban out the back. The record carries the manual-rule tier and the signature (class, paths). The same record reaches the enforcer the scored path uses; a deployed enforcer publishes the kernel row the same way. The verdict signals name the rule: the consult-signal test in #252 pins verdict.action=ban, verdict.rules=OPS-MANUAL-01 and verdict.dos.sig.

Keep the match narrow. action: ban never waits for a score; a wide expression bans whole routes. The fail-open posture: a failed evidence write stays logged; the block decision stands.

See also

Provenance

Verified against trishula@798beb1 on main, 2026-10-25.

Smoke run, 2026-10-25:

  • The pack, the fold and the handler tests run in-package: the 403, decision headers, evidence line, clean forward (#248).
  • The consult-signal run: the verdict span carries the action, the rule id and the signature (#252).
  • The in-VM acceptance for the kernel legs: recorded on #253 with the run transcript.