Add an IP blocklist and a per-client rate limit #8

Merged
james merged 1 commit from ip-reputation-rate-limit into main 2026-10-08 22:39:10 +00:00
Owner

Two checks that judge the client rather than the request. Both run before the WAF, cost no model call, and are off until configured.

Pipeline order is now: shunned client, IP blocklist, bypass prefix, rate limit, WAF, verdict cache, gate, model.

IP blocklist

Variable Default Meaning
CENTINELA_IP_BLOCKLIST unset comma-separated files and http(s) URLs
CENTINELA_IP_BLOCKLIST_REFRESH 1h how often every source is re-read
  • Format: one address or CIDR per line, IPv4 or IPv6; # and ; start a comment. Spamhaus DROP and FireHOL netsets load as published.
  • Effect: a listed client gets 403, source ip-reputation, on every path including bypass prefixes.
  • Failure handling: a source that fails to refresh, or answers with something that holds no addresses (an error page), keeps its previous contents. A missing file stops startup. A URL that cannot be fetched at startup is logged and retried at the next refresh; until then that feed blocks nothing.
  • Feeds are capped at 64 MB and fetched with a 30 s timeout.

Rate limit

Variable Default Meaning
CENTINELA_RATE_LIMIT 0 (off) requests one client may make per window
CENTINELA_RATE_WINDOW 1m the window
CENTINELA_RATE_LIMIT_EXEMPT unset comma-separated addresses and CIDRs never limited
  • Effect: past the limit the proxy answers 429 with Retry-After; the forward-auth endpoint answers 403, the only refusal an auth subrequest carries. Source rate-limit.
  • Not counted: requests under a bypass prefix, exempt clients, and requests with no client address.
  • Logging: one client rate limited line per client per window, not one per refused request. Refused requests are still counted in /debug/vars.

Limits

  • Rate-limit windows are fixed, and counts are per instance and in memory. With several replicas a client can make up to the limit on each.
  • Both checks trust CENTINELA_CLIENT_IP_HEADER.
  • Rate-limited requests do not count toward the repeat-offender shun.
  • There is no allowlist for the blocklist: a client on a feed is refused even if it is in CENTINELA_RATE_LIMIT_EXEMPT.

Tests

  • internal/iplist: parsing both comment styles, IPv4/IPv6/mapped lookups, file and URL sources, refresh picking up changes, a failed refresh and an error page keeping the last good copy, startup with a missing file and with a feed that is down.
  • Engine: a listed client is refused on any path with no model call; the rate limit refuses past the limit, separates clients, honours exemptions and bypass paths, and resets with the window.
  • Proxy: an over-limit request gets 429 with Retry-After and never reaches the upstream.

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

Merging to main runs build-and-push, which tags and publishes the next patch version.

Two checks that judge the client rather than the request. Both run before the WAF, cost no model call, and are off until configured. Pipeline order is now: shunned client, IP blocklist, bypass prefix, rate limit, WAF, verdict cache, gate, model. ## IP blocklist | Variable | Default | Meaning | |---|---|---| | `CENTINELA_IP_BLOCKLIST` | unset | comma-separated files and `http(s)` URLs | | `CENTINELA_IP_BLOCKLIST_REFRESH` | `1h` | how often every source is re-read | - **Format:** one address or CIDR per line, IPv4 or IPv6; `#` and `;` start a comment. Spamhaus DROP and FireHOL netsets load as published. - **Effect:** a listed client gets `403`, source `ip-reputation`, on every path including bypass prefixes. - **Failure handling:** a source that fails to refresh, or answers with something that holds no addresses (an error page), keeps its previous contents. A missing file stops startup. A URL that cannot be fetched at startup is logged and retried at the next refresh; until then that feed blocks nothing. - Feeds are capped at 64 MB and fetched with a 30 s timeout. ## Rate limit | Variable | Default | Meaning | |---|---|---| | `CENTINELA_RATE_LIMIT` | `0` (off) | requests one client may make per window | | `CENTINELA_RATE_WINDOW` | `1m` | the window | | `CENTINELA_RATE_LIMIT_EXEMPT` | unset | comma-separated addresses and CIDRs never limited | - **Effect:** past the limit the proxy answers `429` with `Retry-After`; the forward-auth endpoint answers `403`, the only refusal an auth subrequest carries. Source `rate-limit`. - **Not counted:** requests under a bypass prefix, exempt clients, and requests with no client address. - **Logging:** one `client rate limited` line per client per window, not one per refused request. Refused requests are still counted in `/debug/vars`. ## Limits - Rate-limit windows are fixed, and counts are per instance and in memory. With several replicas a client can make up to the limit on each. - Both checks trust `CENTINELA_CLIENT_IP_HEADER`. - Rate-limited requests do not count toward the repeat-offender shun. - There is no allowlist for the blocklist: a client on a feed is refused even if it is in `CENTINELA_RATE_LIMIT_EXEMPT`. ## Tests - `internal/iplist`: parsing both comment styles, IPv4/IPv6/mapped lookups, file and URL sources, refresh picking up changes, a failed refresh and an error page keeping the last good copy, startup with a missing file and with a feed that is down. - Engine: a listed client is refused on any path with no model call; the rate limit refuses past the limit, separates clients, honours exemptions and bypass paths, and resets with the window. - Proxy: an over-limit request gets `429` with `Retry-After` and never reaches the upstream. `make vet`, `make build` and `make test` (race detector) pass. Merging to `main` runs `build-and-push`, which tags and publishes the next patch version.
Add an IP blocklist and a per-client rate limit
All checks were successful
security-scan / security-scan (pull_request) Successful in 31s
test / go (pull_request) Successful in 2m5s
7beb9b32bb
Two checks that judge the client rather than the request. Both run
before the WAF, cost no model call, and are off until configured.

CENTINELA_IP_BLOCKLIST names files and http(s) URLs holding one address
or CIDR per line, the format Spamhaus DROP and FireHOL publish. A listed
client is refused first of all, with source ip-reputation. Sources are
re-read every CENTINELA_IP_BLOCKLIST_REFRESH (1h); one that fails, or
returns something with no addresses in it, keeps its previous contents.
A missing file stops startup, an unreachable URL does not.

CENTINELA_RATE_LIMIT is the number of requests one client may make per
CENTINELA_RATE_WINDOW (1m). Past it the proxy answers 429 with
Retry-After and the forward-auth endpoint 403, with source rate-limit.
Bypass paths are not counted and CENTINELA_RATE_LIMIT_EXEMPT lists
addresses that are never limited. A limited client is logged once per
window, not once per refused request.

Rate-limit counts are per instance and in memory, as the shun list is.

🔎 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/7beb9b32bb627b8b1f616e204051c36a2e6df1ea/Dockerfile#L1" target="_blank" rel="noopener noreferrer">Dockerfile:1</a> | image has no HEALTHCHECK | </details>
james merged commit 38e080ecc2 into main 2026-10-08 22:39:10 +00:00
james deleted branch ip-reputation-rate-limit 2026-10-08 22:39:11 +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!8
No description provided.