feat(gateway): temporary IP ban (fail2ban) + honeypot/honeytoken (PRERELEASE AG) #97

Merged
developer merged 3 commits from feature/abuse-ip-ban-honeypot into development 2026-06-21 07:28:31 +00:00
Owner

What

Prod-only, in-memory temporary IP ban (fail2ban-style) enforced at the gateway edge, fed by three signals:

  • Sustained rate-limiter rejections — the IP-keyed public/email/admin classes (the user class stays the backend soft-flag's concern). A hook in the single noteRateLimited choke point.
  • Honeypot tripwire — the contour caddy tags classic scanner-bait paths (/.env, /.git, /wp-*, …) with X-Scrabble-Honeypot and routes them to the gateway; the decoy list is a single source of truth in the Caddyfile.
  • Honeytoken — a planted bearer (GATEWAY_HONEYTOKEN) compared constant-time in resolve.

A banned IP is refused with 429 by the abuseGuard middleware (inside h2c, above the mux) before any work — covering the Connect edge, the live stream and the static SPA/landing the per-op limiter never gated (closes the R3 gap).

Off by default (GATEWAY_ABUSE_BAN_ENABLED=false): the ban keys by the real client IP, which the shared-NAT test contour does not expose, so a ban there would be self-inflicted. Detection (honeypot/honeytoken) still logs in the contour — only the ban action is gated.

Operator control

Operators see and lift active bans on the admin console Throttled page (new Active IP bans panel + Unban). The gateway syncs its active set to the backend every 30 s (POST /api/v1/internal/bans/sync, new backend/internal/banview) and applies the operator unbans the response returns (≤30 s latency).

Out of scope (explicit)

Volumetric L3/L4 DDoS (upstream/CDN), ban persistence (in-memory like ratewatch), an active-bans Grafana gauge (trimmed — the console view + gateway_abuse_banned_total counter cover it).

Verification

  • go build / go vet / gofmt -l clean across gateway + backend; -tags=integration type-checks.
  • Unit: ratelimit (banlist threshold/expiry/unban/disabled/sweep), connectsrv (abuseGuard 429, honeypot 404+ban, disabled passthrough, honeytoken, public-strike bans / user-class does not), config, backendclient (SyncBans), banview.
  • Full backend integration suite green (Postgres testcontainers).
  • docker compose config validates with the new gateway env.

Docs baked in the same PR: ARCHITECTURE §11/§12, FUNCTIONAL (+ru), both READMEs, PRERELEASE (phase AG).

## What Prod-only, in-memory **temporary IP ban** (fail2ban-style) enforced at the gateway edge, fed by three signals: - **Sustained rate-limiter rejections** — the IP-keyed public/email/admin classes (the user class stays the backend soft-flag's concern). A hook in the single `noteRateLimited` choke point. - **Honeypot tripwire** — the contour caddy tags classic scanner-bait paths (`/.env`, `/.git`, `/wp-*`, …) with `X-Scrabble-Honeypot` and routes them to the gateway; the decoy list is a single source of truth in the Caddyfile. - **Honeytoken** — a planted bearer (`GATEWAY_HONEYTOKEN`) compared constant-time in `resolve`. A banned IP is refused with **429** by the `abuseGuard` middleware (inside h2c, above the mux) before any work — covering the Connect edge, the live stream and the **static SPA/landing** the per-op limiter never gated (closes the R3 gap). **Off by default** (`GATEWAY_ABUSE_BAN_ENABLED=false`): the ban keys by the real client IP, which the shared-NAT test contour does not expose, so a ban there would be self-inflicted. Detection (honeypot/honeytoken) still **logs** in the contour — only the ban *action* is gated. ## Operator control Operators see and lift active bans on the admin console **Throttled** page (new **Active IP bans** panel + **Unban**). The gateway syncs its active set to the backend every 30 s (`POST /api/v1/internal/bans/sync`, new `backend/internal/banview`) and applies the operator unbans the response returns (≤30 s latency). ## Out of scope (explicit) Volumetric L3/L4 DDoS (upstream/CDN), ban persistence (in-memory like `ratewatch`), an active-bans Grafana gauge (trimmed — the console view + `gateway_abuse_banned_total` counter cover it). ## Verification - `go build` / `go vet` / `gofmt -l` clean across gateway + backend; `-tags=integration` type-checks. - Unit: `ratelimit` (banlist threshold/expiry/unban/disabled/sweep), `connectsrv` (abuseGuard 429, honeypot 404+ban, disabled passthrough, honeytoken, public-strike bans / user-class does not), `config`, `backendclient` (SyncBans), `banview`. - Full backend **integration** suite green (Postgres testcontainers). - `docker compose config` validates with the new gateway env. Docs baked in the same PR: `ARCHITECTURE` §11/§12, `FUNCTIONAL` (+ru), both READMEs, `PRERELEASE` (phase **AG**).
developer added 1 commit 2026-06-21 06:54:47 +00:00
feat(gateway): temporary IP ban (fail2ban) fed by rejections + honeypot/honeytoken
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m11s
041106d623
Add a prod-only, in-memory IP ban enforced at the edge, fed by three signals:
sustained rate-limiter rejections (the IP-keyed public/email/admin classes — the
user class stays the backend soft-flag's concern), a honeypot decoy-path hit (the
contour caddy tags decoys with X-Scrabble-Honeypot and routes them to the gateway),
and a honeytoken (a planted bearer, GATEWAY_HONEYTOKEN). A banned IP is refused with
429 by the abuseGuard middleware before any work — covering the Connect edge, the
live stream and the static SPA/landing the per-op limiter never gated.

The ban is off by default: it keys by the real client IP the shared-NAT test contour
does not expose, so a ban there would be self-inflicted; detection still logs in the
contour, only the ban action is gated (GATEWAY_ABUSE_BAN_ENABLED). Rejection bans last
GATEWAY_ABUSE_BAN_DURATION; tripwire/honeytoken hits are near-zero-false-positive and
earn longer fixed bans. Each ban increments gateway_abuse_banned_total{reason}.

Operators see and lift active bans on the admin console's Throttled page; the gateway
syncs its active set to the backend every 30s (POST /api/v1/internal/bans/sync,
backend/internal/banview) and applies the operator unbans the response returns.

PRERELEASE phase AG. Docs baked into ARCHITECTURE / FUNCTIONAL (+ru) / both READMEs.
developer added 1 commit 2026-06-21 06:58:12 +00:00
docs(prerelease): mark phase AG done (code + test contour)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
81b44c2b02
developer added 1 commit 2026-06-21 07:02:25 +00:00
fix(deploy): honeypot tag dropped — Caddy applies header_up delete after set
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
cf9fa75d62
The @honeypot block both deleted and set X-Scrabble-Honeypot in one reverse_proxy.
Caddy applies header_up deletions *after* sets, so the tag we set was immediately
stripped: the gateway never saw it, and a decoy hit (e.g. GET /.env) fell through
to the gateway's /app redirect (308) instead of tripping the honeypot. Drop the
delete — the bare set already replaces any client-supplied value. The real
endpoints keep stripping the header in the @gateway block (delete-only, no
conflicting set). Caught on the live test contour (no caddy locally).
owner approved these changes 2026-06-21 07:28:01 +00:00
developer merged commit e79c1ea891 into development 2026-06-21 07:28:31 +00:00
developer deleted branch feature/abuse-ip-ban-honeypot 2026-06-21 07:28:31 +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#97