type: decision
status: active
timestamp: 2026-06-20
tags: [security, anti-bot, decisions, defense-in-depth, cloudflare, turnstile, hono]
status: active
timestamp: 2026-06-20
tags: [security, anti-bot, decisions, defense-in-depth, cloudflare, turnstile, hono]
Anti-bot — defense in depth (CF WAF + Turnstile + Hono rate-limit)
Bot defense: CF WAF + Turnstile + Hono rate-limit. All free
Anti-bot — defense in depth (CF WAF + Turnstile + Hono rate-limit)
Decision
The family runs three anti-bot layers, each at a different stage of the request lifecycle. A request must pass all three to reach a route handler. All three are free, no card, and run on infrastructure the family is already using.
| Layer | Stage | Service | What it blocks |
|---|---|---|---|
| 1 | Edge (zone-wide) | Cloudflare WAF + Bot Fight Mode | Known-bad IPs, common attack patterns (SQLi/XSS/RFI), obvious bot signatures, DDoS |
| 2 | Form-submit boundary | Cloudflare Turnstile (with hCaptcha fallback) | Automated form submissions on contact / sign-up / comment forms |
| 3 | API per-route throttle | Hono rate-limit middleware | Per-IP per-route abuse on the api.oriz.in Worker |
Why three layers (not one)
The user’s direction was ”+ Turnstile (already locked)”. The two sibling layers (WAF + rate-limit) ride alongside because:
- Each catches what the other misses — WAF can’t see per-route intent (everything looks like the same zone); Turnstile only fires on form-submit (most API endpoints aren’t form-submits); Hono rate-limit can’t see traffic the WAF already dropped at the edge.
- Each is on different substrate — a misconfiguration / outage / quota cliff on one layer is recoverable from the other two: WAF down ? Turnstile + rate-limit still gate; Turnstile rejected by region ? hCaptcha takes over; Worker over-quota ? WAF still drops the floor.
- All three are free on the family’s existing Cloudflare account — zero incremental cost, zero new vendors.
This is the same defense-in-depth pattern as the double security-headers audit (securityheaders.com + Mozilla Observatory) and the two-captcha pair (Turnstile primary + hCaptcha fallback).
Layer detail
1. Cloudflare WAF + Bot Fight Mode (edge)
- Configured per zone (
oriz.in,*.oriz.in). - Free managed ruleset; auto-updated by Cloudflare.
- Bot Fight Mode flags + challenges signature-known bots.
- Coarse zone-wide rate-limit rule (10K req/10min) as DDoS safety net.
- Runs before any Worker / Pages serve; zero CPU on origin.
2. Turnstile + hCaptcha fallback (form-submit)
- The shared
<Captcha>component in gates every unauthenticated POST surface —<ContactForm>, sign-up, comment boxes — per the locked Turnstile + hCaptcha pair decision. - The Worker verifies the token server-side via
challenges.cloudflare.com/turnstile/v0/siteverify(or hCaptcha’s equivalent) before reading the form payload. - App Check + reCAPTCHA Enterprise still front Firestore writes — different attack surface, different layer; not affected by this decision.
3. Hono rate-limit middleware (API per-route)
- Fine-grained per-route, per-IP, sliding-window throttle.
- Lives in
@chirag127/oriz-kit/serverso every Worker route imports the same middleware. - Backed by Workers KV — runs on existing Worker + KV free tier per the worker quota mitigation playbook.
- Each route declares its own budget (
10/minfor contact,100/minfor OG,1000/minfor feed reads).
Implications
- Every site inherits all three layers automatically — they ride
on shared infrastructure (Cloudflare zone, oriz-kit, api.oriz.in
Worker), no per-site configuration beyond mounting
<Captcha>in forms. - CSP allow-list in
_headerspreset already permitschallenges.cloudflare.com(Turnstile) and hCaptcha origin; no per-site CSP exception. - Observability — the api.oriz.in Worker logs rate-limit trip events to Axiom; Cloudflare WAF event log is queryable in the dashboard. PR-time alarms not yet wired (TODO).
- Incident playbook — if a flood breaks through (bypasses all
three layers), the response is:
- Tighten Hono rate-limit on the affected route in oriz-kit, ship.
- Add a Cloudflare custom WAF rule (free plan: up to 5) for the observed pattern.
- Flip the affected form’s
<Captcha>tointeractivemode (Turnstile interactive challenge) instead of invisible.
- No card-on-file — none of the three layers requires the Cloudflare Workers Paid plan, Cloudflare Pro plan, or any other paid Cloudflare product. The family stays on the free-plan posture.
Cross-refs
- Cloudflare WAF service
- Cloudflare Turnstile service
- hCaptcha service — Turnstile fallback
- Hono rate-limit service
- Captcha pair decision (Turnstile + hCaptcha) — sibling
- Security headers strategy decision — adjacent layer
- Umbrella Hono Worker decision
- CF Worker quota mitigation playbook
- No card-on-file rule