Shun a client after repeated blocks #7

Merged
james merged 1 commit from repeat-offender-shun into main 2026-10-08 22:06:43 +00:00
Owner

Stacked on #5. This branch is built on inline-review so that late verdicts count; until #5 merges, the diff here includes its commit. Merge #5 first.

Problem

A shun needed one request severe enough to earn it (severity 2.9 under balanced). On 8 October a scanner from 34.85.33.104 sent 88 requests to one host in 45 seconds. The model blocked 26 of them, the highest severity was 2.79, and the client was never shunned, so every later request was judged on its own and 59 failed open.

Change

Two settings, off by default:

Variable Default Meaning
CENTINELA_SHUN_AFTER 0 (off) blocked requests from one client that earn a shun
CENTINELA_SHUN_WINDOW 1m the window they must fall in

The shun lasts CENTINELA_SHUN_TTL, as any other.

What counts as a blocked request:

  • a model block, answered in time (model) or late (review)
  • a block served from the verdict cache
  • a WAF hard block

What does not:

  • fail-closed: a model outage says nothing about the client
  • requests refused because the client is already shunned, so a shun does not extend itself
  • flag verdicts

Each shun is logged as client shunned with the count and window, and counted in centinela_repeat_offender_shuns on /debug/vars.

Limits

  • The count is per instance and in memory, like the shun list. With two replicas a client needs the threshold on one of them.
  • The window is fixed from the client's first block, not sliding.
  • Clients are identified by CENTINELA_CLIENT_IP_HEADER; users behind one NAT address share a count.

Tests

  • Model block, cache block and hard block add up to a shun at the threshold and not before; another client sending the same cached payload once is unaffected.
  • Blocks in separate windows do not add up; fail-closed blocks do not count; 0 disables it.
  • Late verdicts from reviewed misses count.

make vet, make build and make test (race detector) pass.

**Stacked on #5.** This branch is built on `inline-review` so that late verdicts count; until #5 merges, the diff here includes its commit. Merge #5 first. ## Problem A shun needed one request severe enough to earn it (severity 2.9 under `balanced`). On 8 October a scanner from 34.85.33.104 sent 88 requests to one host in 45 seconds. The model blocked 26 of them, the highest severity was 2.79, and the client was never shunned, so every later request was judged on its own and 59 failed open. ## Change Two settings, off by default: | Variable | Default | Meaning | |---|---|---| | `CENTINELA_SHUN_AFTER` | `0` (off) | blocked requests from one client that earn a shun | | `CENTINELA_SHUN_WINDOW` | `1m` | the window they must fall in | The shun lasts `CENTINELA_SHUN_TTL`, as any other. What counts as a blocked request: - a model block, answered in time (`model`) or late (`review`) - a block served from the verdict cache - a WAF hard block What does not: - `fail-closed`: a model outage says nothing about the client - requests refused because the client is already shunned, so a shun does not extend itself - `flag` verdicts Each shun is logged as `client shunned` with the count and window, and counted in `centinela_repeat_offender_shuns` on `/debug/vars`. ## Limits - The count is per instance and in memory, like the shun list. With two replicas a client needs the threshold on one of them. - The window is fixed from the client's first block, not sliding. - Clients are identified by `CENTINELA_CLIENT_IP_HEADER`; users behind one NAT address share a count. ## Tests - Model block, cache block and hard block add up to a shun at the threshold and not before; another client sending the same cached payload once is unaffected. - Blocks in separate windows do not add up; fail-closed blocks do not count; `0` disables it. - Late verdicts from reviewed misses count. `make vet`, `make build` and `make test` (race detector) pass.
Review inline model misses in the background instead of dropping them
Some checks failed
security-scan / security-scan (pull_request) Failing after 26s
test / go (pull_request) Successful in 6m10s
072d14c7f5
In inline mode a model call that missed the budget was cancelled and the
request failed open with nothing else done: no verdict cached, no shun,
no training-log line. With a backend that answers one request at a time
this is the common case, not the rare one: a page firing several
escalated requests queues past the budget, and in one 30-minute window
117 of 118 model calls ended that way.

The inline call now runs on its own context with the async deadline of
ten budgets. The request is still held for one budget and blocked if the
model answers in time. On a miss it gets the fail-open (or fail-closed)
answer and the call already in flight is left to finish, so the model is
not paid for twice. Its verdict is cached, shunned on, written to the
training log and logged, as an async result is.

CENTINELA_QUEUE_DEPTH bounds how many calls may run past their budget at
once; beyond it a miss is abandoned and reported. No new settings.

New sources, so a dashboard can tell these apart from a true fail-open:
review-queued (allowed on a miss), review (the late verdict),
review-failed (the model never answered) and review-dropped (bound hit).
fail-open now means the model returned an error inside the budget.
Under CENTINELA_FAIL_OPEN=false a miss still blocks as fail-closed and
is still reviewed, so a retry can be answered from the cache.
Shun a client after repeated blocks
Some checks failed
security-scan / security-scan (pull_request) Failing after 23s
test / go (pull_request) Successful in 6m43s
1cff648677
A shun needed one request severe enough to earn it. A scanner from
34.85.33.104 sent 88 requests in 45 seconds; 26 were blocked by the
model, none reached the shun severity, and the client was never shunned.

CENTINELA_SHUN_AFTER shuns a client once that many of its requests have
been blocked within CENTINELA_SHUN_WINDOW (default 1m), for
CENTINELA_SHUN_TTL as any other shun. It is off by default.

Blocks by the model, in time or from a background review, from the
verdict cache and by the WAF hard block all count. A fail-closed block
does not: a model outage says nothing about the client. Requests refused
because the client is already shunned do not extend the shun.

The count is per instance and in memory, like the shun list.

🔎 ojo scan results

Severity Count
🟠 HIGH 2
🟡 MEDIUM 4
🟢 LOW 1
Details (7)
Type Severity ID/Rule Location Description
vuln 🟠 HIGH GHSA-6gcq-wc29-5xf2 github.com/corazawaf/coraza/[email protected] Coraza JSON body processor: argument-limit truncation reopens an unbounded-depth gjson.Valid stack overflow (process crash)
vuln 🟠 HIGH CVE-2026-41510 github.com/corazawaf/coraza/[email protected] Coraza: Silent argument drop at ArgumentLimit allows bypass of ARGS-targeted rules via parameter flooding
vuln 🟡 MEDIUM GHSA-3wr7-993q-jrff github.com/corazawaf/coraza/[email protected] Coraza: Multipart filename* (RFC 5987) charset restriction lets a decoy filename bypass FILES-based rules
vuln 🟡 MEDIUM GHSA-5gj4-9gm7-2fx2 github.com/corazawaf/coraza/[email protected] Coraza body processor has a JSON key collision that allows unauthenticated attackers to bypass OWASP CRS inspection
vuln 🟡 MEDIUM GHSA-g4qm-m288-5cp9 github.com/corazawaf/coraza/[email protected] Coraza has Cookie Parser Confusion
vuln 🟡 MEDIUM GHSA-w253-m66g-rx24 github.com/corazawaf/coraza/[email protected] Coraza: URL-encoded form Content-Type parameters bypass Coraza body inspection
misconfig 🟢 LOW dockerfile-no-healthcheck Dockerfile:1 image has no HEALTHCHECK
<!-- ojo-scan-summary --> ### 🔎 ojo scan results | Severity | Count | |---|---| | 🟠 HIGH | 2 | | 🟡 MEDIUM | 4 | | 🟢 LOW | 1 | <details><summary>Details (7)</summary> | Type | Severity | ID/Rule | Location | Description | |---|---|---|---|---| | vuln | 🟠 HIGH | <a href="https://github.com/corazawaf/coraza/security/advisories/GHSA-6gcq-wc29-5xf2" target="_blank" rel="noopener noreferrer">GHSA-6gcq-wc29-5xf2</a> | github.com/corazawaf/coraza/[email protected] | Coraza JSON body processor: argument-limit truncation reopens an unbounded-depth gjson.Valid stack overflow (process crash) | | vuln | 🟠 HIGH | <a href="https://github.com/corazawaf/coraza/security/advisories/GHSA-6r3q-mjv7-xr8m" target="_blank" rel="noopener noreferrer">CVE-2026-41510</a> | github.com/corazawaf/coraza/[email protected] | Coraza: Silent argument drop at ArgumentLimit allows bypass of ARGS-targeted rules via parameter flooding | | vuln | 🟡 MEDIUM | <a href="https://github.com/corazawaf/coraza/security/advisories/GHSA-3wr7-993q-jrff" target="_blank" rel="noopener noreferrer">GHSA-3wr7-993q-jrff</a> | github.com/corazawaf/coraza/[email protected] | Coraza: Multipart filename* (RFC 5987) charset restriction lets a decoy filename bypass FILES-based rules | | vuln | 🟡 MEDIUM | <a href="https://github.com/corazawaf/coraza/security/advisories/GHSA-5gj4-9gm7-2fx2" target="_blank" rel="noopener noreferrer">GHSA-5gj4-9gm7-2fx2</a> | github.com/corazawaf/coraza/[email protected] | Coraza body processor has a JSON key collision that allows unauthenticated attackers to bypass OWASP CRS inspection | | vuln | 🟡 MEDIUM | <a href="https://github.com/corazawaf/coraza/security/advisories/GHSA-g4qm-m288-5cp9" target="_blank" rel="noopener noreferrer">GHSA-g4qm-m288-5cp9</a> | github.com/corazawaf/coraza/[email protected] | Coraza has Cookie Parser Confusion | | vuln | 🟡 MEDIUM | <a href="https://github.com/corazawaf/coraza/security/advisories/GHSA-w253-m66g-rx24" target="_blank" rel="noopener noreferrer">GHSA-w253-m66g-rx24</a> | github.com/corazawaf/coraza/[email protected] | Coraza: URL-encoded form Content-Type parameters bypass Coraza body inspection | | misconfig | 🟢 LOW | dockerfile-no-healthcheck | <a href="https://git.colibrisec.org/ColibriSec/centinela/src/commit/1cff64867704ccde51da3e661c4b543d08b4f53f/Dockerfile#L1" target="_blank" rel="noopener noreferrer">Dockerfile:1</a> | image has no HEALTHCHECK | </details>
james merged commit c53d94215f into main 2026-10-08 22:06:43 +00:00
james deleted branch repeat-offender-shun 2026-10-08 22:06:45 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ColibriSec/centinela!7
No description provided.