feat(edge): community IP blocklist (Spamhaus DROP) at the gateway #246

Merged
developer merged 1 commits from feature/edge-blocklist into development 2026-07-11 11:39:47 +00:00
Owner

Why

The light, no-custom-caddy alternative to a full CrowdSec: pre-emptively refuse known-bad source IPs at the edge, reusing the existing app-layer enforcement (no second ban authority, no edge image rebuild). PR2 of 2 (PR1 was the bot health telemetry).

Design (as agreed)

Enforcement stays in the gateway (abuseGuard), so no custom caddy — a client whose IP is in a curated CIDR feed (Spamhaus DROP) gets 403, checked right after the fail2ban ban.

  • ratelimit.Blocklist — a separate static CIDR set (not the per-IP fail2ban store; a bulk CIDR feed can't expand to per-IP entries). Sorted IPv4 ranges + binary search (no new deps); an allowlist (own infra, monitoring) is checked first and never blocked. IPv4 only — a v6 client is never blocked here (fail2ban / honeypot still cover it).
  • Fault-tolerant + admit-legit (your priority): a failed fetch keeps the last-good feed; once older than MAX_STALENESS (48h) it is dropped fail-open — better to under-block than block a legit client on a frozen feed. Bounded fetch (timeout + size cap). Off by default, prod-only (same real-client-IP reason as the ban).
  • A background refresher re-fetches every few hours.

Config (opt-in)

GATEWAY_BLOCKLIST_{ENABLED,URL,ALLOW,REFRESH,MAX_STALENESS} — wired through compose + write-prod-env.sh + prod-deploy/rollback (PROD_GATEWAY_BLOCKLIST_*). Enable it after verifying the feed URL; URL is required when enabled (no feed URL is assumed).

Observability

gateway_blocklist_blocked_total + the gateway_blocklist_entries / gateway_blocklist_age_seconds gauges; a Grafana alert warns when the feed stops refreshing (before it is dropped); service-overview panels.

Verification (local)

gofmt/vet clean · all four modules build · unit tests: CIDR match (range bounds, /32, /28), allowlist override, IPv6 skip, ParseDROP (comments / bare IP / non-v4 / garbage), ApplyRefresh (updated / kept-fresh / dropped-stale) · a 403 abuse-guard integration test (a blocklisted client IP) · compose config + workflow YAML validate.

Note

No test-contour effect — off by default (the shared-NAT contour would self-block). The /offer/-style edge probe isn't needed: enforcement is app-side, no caddy route change.

## Why The light, no-custom-caddy alternative to a full CrowdSec: pre-emptively refuse **known-bad source IPs** at the edge, reusing the existing app-layer enforcement (no second ban authority, no edge image rebuild). PR2 of 2 (PR1 was the bot health telemetry). ## Design (as agreed) Enforcement stays **in the gateway** (`abuseGuard`), so **no custom caddy** — a client whose IP is in a curated CIDR feed (Spamhaus DROP) gets **403**, checked right after the fail2ban ban. - **`ratelimit.Blocklist`** — a **separate** static CIDR set (not the per-IP fail2ban store; a bulk CIDR feed can't expand to per-IP entries). Sorted IPv4 ranges + **binary search** (no new deps); an **allowlist** (own infra, monitoring) is checked first and never blocked. **IPv4 only** — a v6 client is never blocked here (fail2ban / honeypot still cover it). - **Fault-tolerant + admit-legit** (your priority): a failed fetch **keeps the last-good feed**; once older than `MAX_STALENESS` (48h) it is **dropped fail-open** — better to under-block than block a legit client on a frozen feed. Bounded fetch (timeout + size cap). **Off by default, prod-only** (same real-client-IP reason as the ban). - A background refresher re-fetches every few hours. ## Config (opt-in) `GATEWAY_BLOCKLIST_{ENABLED,URL,ALLOW,REFRESH,MAX_STALENESS}` — wired through compose + `write-prod-env.sh` + prod-deploy/rollback (`PROD_GATEWAY_BLOCKLIST_*`). Enable it after verifying the feed URL; `URL` is required when enabled (no feed URL is assumed). ## Observability `gateway_blocklist_blocked_total` + the `gateway_blocklist_entries` / `gateway_blocklist_age_seconds` gauges; a Grafana alert warns when the feed stops refreshing (before it is dropped); service-overview panels. ## Verification (local) `gofmt`/`vet` clean · all four modules build · unit tests: CIDR match (range bounds, /32, /28), allowlist override, IPv6 skip, `ParseDROP` (comments / bare IP / non-v4 / garbage), `ApplyRefresh` (updated / kept-fresh / dropped-stale) · a **403** abuse-guard integration test (a blocklisted client IP) · compose config + workflow YAML validate. ## Note No test-contour effect — off by default (the shared-NAT contour would self-block). The `/offer/`-style edge probe isn't needed: enforcement is app-side, no caddy route change.
developer added 1 commit 2026-07-11 11:33:52 +00:00
feat(edge): community IP blocklist (Spamhaus DROP) at the gateway
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 24s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m49s
4ba9da6721
Refuse a client whose IP is in a curated CIDR feed with 403 in the same
abuseGuard, before the fail2ban ban. Prod-only (keys by real client IP), off by
default.

- ratelimit.Blocklist: a sorted-range IPv4 matcher (binary search) with an
  allowlist checked first; ParseDROP reads the feed; ApplyRefresh keeps the
  last-good feed on a transient fetch failure and drops it fail-open once stale
  (better to under-block than block a legitimate client on a frozen feed). A
  separate static CIDR set, not the per-IP fail2ban store.
- gateway: a refresher goroutine re-fetches every few hours (bounded fetch + size
  cap); config GATEWAY_BLOCKLIST_{ENABLED,URL,ALLOW,REFRESH,MAX_STALENESS}.
  IPv6 is not matched (a v6 client is still covered by fail2ban / the honeypot).
- observability: gateway_blocklist_blocked_total + entries/age gauges; a Grafana
  alert warns before the feed is dropped; service-overview panels.
- deploy: compose env + write-prod-env.sh + prod-deploy/rollback wiring (opt-in
  via PROD_ vars).
- docs (ARCHITECTURE, deploy/README); unit tests (match, allowlist, IPv6 skip,
  parser, fault-tolerance) + a 403 abuse-guard integration test.
owner approved these changes 2026-07-11 11:38:59 +00:00
developer merged commit 3ce460f72a into development 2026-07-11 11:39:47 +00:00
developer deleted branch feature/edge-blocklist 2026-07-11 11:39:47 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#246