feat(edge): community IP blocklist (Spamhaus DROP) at the gateway #246
Reference in New Issue
Block a user
Delete Branch "feature/edge-blocklist"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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).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).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;URLis required when enabled (no feed URL is assumed).Observability
gateway_blocklist_blocked_total+ thegateway_blocklist_entries/gateway_blocklist_age_secondsgauges; a Grafana alert warns when the feed stops refreshing (before it is dropped); service-overview panels.Verification (local)
gofmt/vetclean · 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.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.