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.
- 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/*")- Ship it the way every rule ships: the ConfigMap + the
WAFPolicyCR. 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- 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
- 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 -3would-ban 192.0.2.10 tier=manual-rule score=0 sig={Class:manual-rule Paths:[/v1/chat/completions]}
- 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
- spec 13.7 — the manual and the automatic paths.
- spec 13.3 — the scored path the record shares.
- Issue #237 — the DoS/ban stream. PR #248 — the ban surface. PR #252 — the signals.
- Recipe 6 — the scored ban ladder.
- Recipe 10 — the signature the record carries.
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.