Blob: waf.md
Cloudflare WAF Guide for Your flamemail Deployment
If you are deploying your own flamemail instance, this guide helps you choose a practical WAF setup for the Free plan.
The goal is not to eliminate all abuse at the edge. The goal is to reduce cheap HTTP abuse before it reaches your Worker, while keeping the application usable for real visitors.
What you are protecting
All unauthenticated routes live under /api/public. Inbox routes under /api/protected/inboxes/* use a bearer token (or an admin cookie when the request includes ?admin=1). Admin routes under /api/protected/admin/* use the __Host-flamemail-admin cookie minted at the tessera OIDC callback. This split makes WAF rules easy to target.
| Surface | Method | Auth | Notes |
|---|---|---|---|
/, /about, /admin, /link, /inbox/:address |
GET | None | SPA pages and route shells |
/api/public/config |
GET | None | Bootstrap config (Turnstile site key) |
/api/public/domains |
GET | None | Read-only domain list |
/api/public/inboxes |
POST | None | Creates inbox state in D1 and KV |
/api/public/admin/start |
GET | None | Begins tessera OIDC handshake, sets __Host-flamemail-oidc cookie |
/api/public/admin/callback |
GET | None | Completes OIDC handshake, sets __Host-flamemail-admin cookie |
/api/public/admin/logout |
POST | Admin cookie (best-effort) | Clears KV admin session and the admin cookie |
/api/protected/inboxes/:address |
GET, DELETE | Inbox bearer or admin cookie | Inbox metadata and deletion (?admin=1 selects admin-inspect) |
/api/protected/inboxes/:address/extend |
POST | Inbox bearer | Extend inbox TTL |
/api/protected/inboxes/:address/ws-ticket |
POST | Inbox bearer or admin cookie | Issue one-time WebSocket ticket (?admin=1 selects admin-inspect) |
/api/protected/inboxes/:address/emails/* |
GET, DELETE | Inbox bearer or admin cookie | Email listing, detail, raw, attachments |
/api/protected/admin/* |
GET, POST, PATCH, DELETE | Admin cookie (__Host-) |
Domain and permanent-inbox management |
/ws |
GET | One-time ticket | WebSocket upgrade |
flamemail already includes:
- strict security headers on HTTP responses
- same-origin checks on WebSocket upgrades
- bearer authorization for inbox APIs and tessera-OIDC cookie auth for admin APIs
- Cloudflare Turnstile verification on anonymous inbox creation; admin sign-in is delegated to tessera
- email HTML sanitization and isolated rendering
What flamemail does not include yet is Worker-side throttling and lockout behavior for the public write endpoints. Turnstile adds useful friction, but Cloudflare edge controls should still be treated as a first layer rather than your only layer.
Important: Cloudflare WAF applies to HTTP traffic, including the initial /ws upgrade request. It does not inspect or protect the Worker email() handler. Inbound email abuse needs separate controls inside the application.
Free plan capabilities
| Capability | Limit | Notes |
|---|---|---|
| L7 DDoS mitigation | Unmetered | Included on all plans |
| Free Managed Ruleset | Included | Subset of the full Cloudflare Managed Ruleset, covers high-severity vulnerabilities |
| Custom WAF rules | 5 rules | All actions except Log; no regex (Business+) |
| Rate limiting rules | 1 rule | IP-based counting only; 10 s minimum period |
| Bot Fight Mode | Included | Domain-wide, not configurable, cannot be bypassed with Skip rules |
| Browser Integrity Check | Included | Enabled by default |
| WAF body inspection | Up to 1 MB | Default on Free |
Not available on Free: Cloudflare Managed Ruleset (full), OWASP Core Ruleset, Exposed Credentials Check, WAF Attack Score, Super Bot Fight Mode, Bot Management.
Where abuse is most likely
Highest priority
POST /api/public/inboxes— Anonymous callers can create inbox state, which drives D1 and KV usage. Turnstile helps, but this path is still worth edge rate limiting.GET /api/public/admin/startandGET /api/public/admin/callback— Admin OIDC handshake. tessera handles credential entry and human verification, so flamemail's surface here is mostly redirect handling. The endpoints are still worth covering by edge rate limiting under/api/public/*./ws— Cloudflare only sees the handshake request. After the WebSocket is established, edge protections no longer inspect message traffic.
Secondary priority
/admin— Attackers and scanners will probe this page even if they do not know how your admin flow works.- Recon paths — Generic scanners request
/.git,/.env,/wp-admin,/wp-login.php,/xmlrpc.php,/phpmyadmin, etc. - General
/api/*noise — Malformed requests, probing, and random exploit traffic even for paths that are not valid in flamemail.
Recommended Cloudflare Free configuration
1. Enable the Free Managed Ruleset
This gives you baseline protection against known web exploit patterns with no application changes.
- Enable the default managed ruleset.
- Review Security Events after deployment.
- Avoid aggressive tuning until you have real traffic data.
2. Keep Browser Integrity Check enabled
flamemail is browser-first, so Browser Integrity Check is a reasonable fit. Leave it on unless you find a specific compatibility problem.
3. Use your one rate-limit rule on POST /api/public/*
You only get one rate-limit rule on Free, so spend it on the anonymous write surface.
| Parameter | Value |
|---|---|
| Expression | http.host eq "<YOUR_HOSTNAME>" and http.request.method eq "POST" and starts_with(http.request.uri.path, "/api/public/") |
| Counting | Per IP |
| Threshold | 3-5 requests / 10 s |
| Action | Block |
| Mitigation timeout | 10 s |
This is intentionally conservative. It slows down casual abuse but is not a replacement for application-side throttling. Cloudflare documents rate limiting as approximate, so some excess requests may still reach your Worker before mitigation starts.
Because all unauthenticated write endpoints live under /api/public, this single rule covers inbox creation and the admin OIDC start/callback. If your plan ever upgrades to Pro (2 rate-limit rules), you can add a tighter rule specifically for /api/public/admin/*.
4. Use custom rules for low-risk edge filtering
You have 5 custom rules on Free. Here is a suggested allocation:
Rule 1 — Challenge /admin
http.host eq "<YOUR_HOSTNAME>" and http.request.uri.path eq "/admin" → Managed ChallengeGood fit because this is a normal browser navigation, not a JSON API call.
Rule 2 — Restrict /api/protected/admin/* by IP (if you can)
If you administer from stable IP addresses:
http.host eq "<YOUR_HOSTNAME>" and starts_with(http.request.uri.path, "/api/protected/admin/") and not ip.src in { <ADMIN_IPS> } → BlockIf you do not have stable admin IPs, skip this rule. Do not challenge /api/protected/admin/* — flamemail's admin UI uses SPA API requests, and challenge flows break JSON endpoints. Instead, protect /admin (Rule 1), keep Turnstile enabled on the login form, and add Worker-side lockouts.
Rule 3 — Block common recon paths
http.host eq "<YOUR_HOSTNAME>" and http.request.uri.path in {"/.git" "/.env" "/wp-admin" "/wp-login.php" "/xmlrpc.php" "/phpmyadmin"} → BlockRemoves common scanner noise before it reaches your Worker.
Note: flamemail's ?admin=1 inbox-inspection mode and admin API mutations both require an Origin header matching the deploy hostname (worker-side same-origin gate). Aggressive Origin-stripping at the edge will break legitimate admin actions.
Rule 4 — Reject invalid /ws requests
http.host eq "<YOUR_HOSTNAME>" and http.request.uri.path eq "/ws" and http.request.method ne "GET" → BlockLow-risk cleanup rule. Keep it simple — WebSocket traffic is sensitive to aggressive edge filtering.
Rule 5 — Reject suspicious cross-origin API writes (optional)
http.host eq "<YOUR_HOSTNAME>" and (http.request.method in {"POST" "PATCH" "DELETE"}) and starts_with(http.request.uri.path, "/api/") and len(http.request.headers["origin"]) gt 0 and http.request.headers["origin"] ne "https://<YOUR_HOSTNAME>" → BlockOnly use this after testing. Some clients omit Origin, so treat this as hygiene rather than primary protection.
Bot Fight Mode
Bot Fight Mode is available on Free but should be treated as optional, not mandatory.
Why to be cautious:
- It applies across the whole domain with no per-path control.
- It may interfere with API traffic.
- On Free, it cannot be bypassed using WAF Skip rules (unlike Super Bot Fight Mode on paid plans).
If you enable it, test these flows explicitly:
- creating an inbox via
POST /api/public/inboxes - admin sign-in via
GET /api/public/admin/startand the resulting tessera redirect back toGET /api/public/admin/callback - authenticated admin API fetches under
/api/protected/admin/* - the
/wsupgrade flow - any uptime checks or synthetic monitoring you run
If Bot Fight Mode causes regressions, disable it rather than trying to work around it.
What Cloudflare will not solve for you
Inbound email abuse
flamemail's email() handler is not an HTTP endpoint, so WAF rules do not protect it.
flamemail already enforces a few inbound guardrails:
- messages larger than 10 MiB are rejected
- messages with more than 10 attachments are rejected
- delivery is rejected when an inbox already holds 100 emails
If you expect real spam pressure, consider adding:
- sender or domain blocklists
- per-sender or per-domain ingestion quotas
- per-attachment or cumulative attachment size caps
- earlier rejection of clearly abusive senders
Application-layer brute force and resource abuse
Cloudflare rate limiting on Free gives you one rule. Turnstile reduces cheap automation. Neither replaces Worker-side throttling, quotas, or lockouts.
For a serious deployment, add Worker-side limits for:
POST /api/public/inboxes— throttle inbox creation per IPGET /api/public/admin/startandGET /api/public/admin/callback— throttle to slow down OIDC handshake replay attempts
Browser token theft through XSS
Temporary inbox tokens are stored in localStorage. The admin session lives in an HttpOnly cookie (__Host-flamemail-admin) so it is not reachable from JavaScript.
flamemail has strong defenses around CSP and email HTML sanitization, but the inbox bearer remains sensitive by nature. WAF can reduce incoming attack traffic but does not remove the need for robust frontend XSS protection.
Suggested rollout
Phase 1 — Cloudflare edge configuration
- Enable the Free Managed Ruleset.
- Confirm Browser Integrity Check is enabled.
- Decide whether to enable Bot Fight Mode. If you do, test it carefully.
- Add your one rate-limit rule targeting
POST /api/public/*. - Add custom rules:
/adminchallenge, recon path blocking, invalid/wsrejection. - Optionally add IP allowlist for
/api/protected/admin/*and cross-origin write blocking.
Phase 2 — Application hardening
- Create a Turnstile widget and configure
TURNSTILE_SITE_KEYplusTURNSTILE_SECRET_KEY. - Verify
GET /api/public/config, inbox creation, and admin login work end-to-end. - Add Worker-side throttling for
POST /api/public/inboxes. - Add Worker-side throttling and short lockouts for
POST /api/public/admin/login.
Phase 3 — Email abuse controls
- Add sender and domain abuse controls.
- Add per-sender or per-domain ingestion quotas.
- Add stricter attachment-size controls if your deployment starts seeing spam pressure.
Recommended baseline at a glance
| Layer | Recommended baseline |
|---|---|
| Managed Rules | Enable the Free Managed Ruleset |
| Browser filtering | Keep Browser Integrity Check enabled |
| Bot controls | Treat Bot Fight Mode as optional; test before keeping it on |
| Rate limiting | 1 rule on Free — use it on POST /api/public/* to cover all unauthenticated writes |
| Custom rules (5) | Challenge /admin, block recon paths, reject invalid /ws, optionally restrict /api/protected/admin/* by IP, optionally block cross-origin writes |
| Form protection | Keep Turnstile enabled on inbox creation and admin login |
| Application controls | Add Worker-side throttling, login lockouts, and email abuse controls |