diff --git a/README.md b/README.md index 31d8641e..c2ab3e6d 100644 --- a/README.md +++ b/README.md @@ -687,6 +687,94 @@ The CMS caches through `cached()` / `cachedQuery()` (`src/lib/cache.ts`): an in- --- +## Cloudflare & Anti-DDoS Protection + +Two layers work together to keep the origin alive during misuse, floods, and +repeat-offender attacks: + +1. **App-layer gate** (production only) — per-IP request buckets per category + (pages / API / auth), a whole-site safety valve, and escalation tiers that + temporarily hard-block offending IPs (`src/lib/ddos-guard.ts`). + `/api/health` is always exempt so the Docker liveness probe never trips it. +2. **Automatic Cloudflare edge blocks** (optional, works on every Cloudflare + plan including Free) — when the gate hard-blocks an IP, the CMS mirrors the + block to the zone's **IP Access Rules** (`src/lib/cloudflare-api.ts`), so a + repeat offender is dropped at the Cloudflare edge before it reaches your + server. Blocks expire together with the gate's tiered block (a 30s sweep + removes them; see `src/instrumentation.ts`). + +### Prerequisites + +- The domain must actually be **proxied through Cloudflare** (orange cloud) — + automatic edge rules can only stop traffic that transits the edge. Restrict + direct access to the origin ports (e.g. only allow your server IP(s), + Cloudflare IPs, or swap the firewall to 443/80 only) so attackers cannot + bypass Cloudflare. +- A Cloudflare API token with the following zone permissions: + - `Zone > Zone > Read` + - `Zone > Firewall > Edit` + Create one at **My Profile → API Tokens → Create Token** (custom token). + +### 1. Environment variables + +`.env.example` documents every anti-DDoS / Cloudflare variable. In your real +`.env`: + +```dotenv +# --- App-layer gate (production only) --- +ANTI_DDOS_ENABLED=true +ANTI_DDOS_PAGES_LIMIT=300 +ANTI_DDOS_PAGES_WINDOW_SEC=60 +ANTI_DDOS_API_LIMIT=600 +ANTI_DDOS_API_WINDOW_SEC=60 +ANTI_DDOS_AUTH_LIMIT=20 +ANTI_DDOS_AUTH_WINDOW_SEC=60 +ANTI_DDOS_GLOBAL_LIMIT=18000 +ANTI_DDOS_GLOBAL_WINDOW_SEC=60 +ANTI_DDOS_GLOBAL_HALT_MS=10000 +ANTI_DDOS_VIOLATION_WINDOW_SEC=600 +ANTI_DDOS_MAX_VIOLATIONS=10 +ANTI_DDOS_BLOCK_TIERS=5:600,20:3600,50:86400 + +# --- Cloudflare API (automatic edge blocks) --- +CLOUDFLARE_API_TOKEN= +CLOUDFLARE_ZONE_ID= +CLOUDFLARE_AUTO_BLOCK_ENABLED=true +``` + +Credentials live in env only and are never stored in Redis-visible config. +Restart the container after changing them. + +### 2. Verify in the admin panel + +Open **DevOps → Anti-DDoS protection** +(`/admin/devops/antiddos`, requires `settings.view`) and: + +- Check the **Cloudflare** card shows the request arrived proxied, and the + green **API configured** badge. +- Click **Verify connection** — it confirms the token can read the zone and + shows the zone name. +- Keep the **Automatically create Cloudflare edge blocks** toggle on. +- The **Cloudflare edge blocks** card lists every auto-created rule with its + remaining TTL; **Remove rule** deletes it — and **Unban** lifts both the + host-block and the edge rule in one action. + +### 3. Notes + +- **Only proxied traffic** is edge-blocked. The gate verifies a request + actually transited Cloudflare (`CF-Ray` / `CDN-Loop`); a spoofable + `CF-Connecting-IP` alone is never trusted. +- **Free plan**: IP Access Rules support is plan-independent; the limit is + ~300 rules per zone and the 30s sweep keeps expired rules cleaned up. +- **Distributed botnets**: many unique IPs each under the threshold are shed + by the whole-site valve (short global halt) rather than banned one-by-one, + which avoids collateral damage to shared IPs. +- **Volumetric L3/L4 floods** cannot be stopped by application code — for + those, keep the domain on Cloudflare's orange cloud so Cloudflare's own + always-on DDoS protection absorbs them. + +--- + ## Production Deployment (PM2) ```bash