How the fabric works
A toolbox is not a longer list of plugins. Fifteen plugins that each block independently produce a gateway nobody can reason about and nobody dares arm.
The fabric is the opposite arrangement: many independent detectors feed one shared judgement, and one component acts on it with a graded response.
reputation ──┐
waf ─────────┤
bots ────────┤
honeypots ───┤──▶ threat score ──▶ threat policy ──▶ log · challenge · throttle · tarpit · deny · ban
traffic ─────┤ (per request) (per route)
logins ──────┤
uploads ─────┤
objects ─────┤
contract ────┘
│
└──▶ incidents ──▶ one alert instead of nine thousand
└──▶ ledger ──▶ cross-request memory ──▶ ban
Signals, score, decision
Every detector publishes signals rather than verdicts. A signal carries a source, a weight (0–100), a tag, and a confidence:
{ "source": "reputation.feed", "kind": "reputation", "weight": 80,
"tag": "feed:firehol-l1", "confidence": 1.0 }
The score is the sum of the weights, capped at 100, each scaled by its confidence. That is the point of having a bus: three weak signals can add up to something worth acting on, which no single detector could ever conclude on its own.
Confidence is clamped to [0, 1], so a detector can be honest about being unsure but cannot inflate
its own weight.
Why a graded response matters
Without the fabric, the only outcomes are 200 and 403. That binary is why blocking mode is frightening: every false positive is a broken customer.
With it, a score of 45 costs a suspect caller nothing but a log line, a challenge or a quota, 75 costs them three seconds, and 95 gets them banned. The same detectors become deployable, because the cost of being wrong drops by an order of magnitude.
What contributes today
| Source | Signal | Weight |
|---|---|---|
| IP reputation (feeds, CrowdSec, ASN) | one per matching source | the source's own weight |
| WAF | waf:match when rules matched without a block decision, waf:blocked when the ruleset reached one — in monitoring too, with contribute, which the preset always sets | weighed by the policy: waf_match_weight 45, waf_block_weight 90 |
| Bot guard | bot:impersonator:<name>, bot:<category>:<name> | 60 for a forged crawler, the rule's weight for a category |
| Honeypots | honeypot:<path or token> | 100 by default |
| Traffic guard | traffic:route_surge, source_surge, consumer_surge, asn_surge | 40 (30 for a network), more past twice and four times the threshold; a route's surge never more |
| Login guard | login:credential_stuffing, password_spraying, account_under_attack, breached_password | 50, 70, 40, 20, more past twice the threshold |
| Upload guard | upload:<reason> | from 10 (too many files) to 80 (polyglot, zip slip) |
| Object guard | objects:surge, sequential, enumeration, with contribute | 40, 50, 50 |
| API contract | api:<mismatch>, with contribute | 20 for a path or method outside the contract, 30 for the rest |
| Ban store | short-circuits before anything else runs | — |
Fail2ban, the error leakage guard and the sensitive data guard act on their own and record their decisions, but add nothing to the score: what they see is already a decision.
The WAF publishes what it found to the bus whatever its mode: rules matching without a block
decision, and a block decision, are two signals, and the policy says what each is worth. The case
worth having is the one the WAF does nothing about itself: a match in monitoring mode still weighs
on the score, against everything else. In blocking mode the
WAF still refuses the request itself; waf_block_decisive makes a block reach the policy's top tier
whatever the sum. See policies.
The two plugins
| Plugin | Runs | Does |
|---|---|---|
| Threat gate | access validation, first | Refuses callers already banned, before any inspection |
| Threat response | request transformation, last | Reads the accumulated score and applies one tier |
Order matters: the gate is deliberately separate so a ban costs a map lookup rather than a full inspection of a request nobody is going to serve. Put the response plugin after the WAF in the route's plugin chain, or it will read a score the WAF has not contributed to yet.
That last sentence is a requirement on whoever configures the route, which is why there is a preset plugin that expands into the whole chain with the ordering already fixed.
Everything is recorded
Whatever the outcome, one normalised CloudApimSecurityEvent is emitted and correlated into an
incident. That happens in dry run too — which is the whole point of dry run.