{
"turnstile": {
"mode": "gate",
"siteKey": "0x4AAAAAAA...",
"secretEnv": "TURNSTILE_SECRET",
"hosts": ["gram.example.com"],
"paths": ["/*"],
"exemptPaths": ["/mcp", "/openapi.json"],
"clearanceTtlSeconds": 1800,
"challenge": { "appearance": "interaction-only" }
},
"routes": [
{
"host": "gram.example.com", "path": "/*", "frontend": true,
"backend": "http://gram:8000"
}
]
}Cloudflare Turnstile
Useful if you want to…
stop bots from submitting a public form without changing the application;
protect an application that has no authentication and no form (a public tool, an API-driven frontend) — the page or only its expensive action;
add the same check to several pages and hosts from one place;
keep the widget working on a page Gate One already proxies.
The block is platform-wide: one configuration entry, every matching page gets the widget or the challenge, every matching request is checked with Cloudflare Siteverify before the backend.
Three modes
| Mode | Protects | Mechanism |
|---|---|---|
| form submissions | widget injected into the matching forms; every matching POST is verified |
| whole pages or actions ( | a challenge at the original URL, then a signed 30-minute clearance cookie; later requests pass without a new Siteverify call |
| gate + form submissions | the gate applies, and form POSTs are verified too |
gate is the right choice for an application with no form: there is no
POST to verify, and the page itself must be protected.
Protecting a whole page
GET /without a clearance is answered by a challenge page at the same URL (no redirect). The backend is not called.The widget solves the challenge and posts the token to
/__gate-one/turnstile/verify. With a Non-Interactive widget,interaction-onlykeeps it hidden (silent verification); setchallenge.appearance: "always"to show its loading spinner.Gate One verifies it, sets
gate_one_turnstile(HttpOnly, 30 min) and redirects to/.The page is served; the backend was called once.
Protecting only the action
To keep the pages public (and indexable) and protect only the expensive endpoint:
"turnstile": {
"mode": "gate",
"paths": ["/api/*"],
"exemptPaths": ["/mcp"],
"preclearance": true
}The page loads normally; the injected script pre-clears in the
background. An API call without clearance gets
403 {"error":"turnstile_required"} with X-Gate-One-Turnstile:
required; the script opens the challenge, exchanges the token and
replays the request. The backend sees one call: the rejected request
never reached it.
Forms
"turnstile": {
"mode": "forms",
"siteKey": "0x4AAAAAAA...",
"secretEnv": "TURNSTILE_SECRET",
"paths": ["/contact/*"],
"forms": ["form#contact"],
"action": "contact"
}The widget is rendered in the matching POST forms and the POST is
verified before the backend. data-gtw-turnstile="off" opts a form out.
both adds this verification to a gate.
Failure modes
| Situation | Status | Body |
|---|---|---|
Missing/invalid token ( |
| readable page or JSON |
Missing clearance, navigation ( |
| challenge page at the original URL |
Missing clearance, fetch/API ( |
|
|
Missing clearance, form POST ( |
| readable page (data cannot be replayed) |
Invalid token at the verify endpoint |
| the challenge page again |
Siteverify unreachable |
|
|
A Siteverify outage is fail-closed, but existing clearance cookies keep working (their signature is checked locally).
CSP
Turnstile needs https://challenges.cloudflare.com in script-src
(and script-src-elem when declared) and in frame-src. When a route
declares a csp block, Gate One appends those sources automatically in
nonce, hash and merge modes. Without a csp block, the page’s own
policy (if any) must allow them.
The limitation to document
The check covers only traffic that passes through this Gate One instance:
a second ingress (nginx, Traefik, Kubernetes Ingress, another Gate One deployment with its own config) that reaches the application directly bypasses it;
internal callers and a published port bypass it.
The invariant is: the application is reachable only through the verification point. Restrict the application’s network access to the Gate One instance and deny everything else.
A gate on / also blocks search-engine crawlers: if the site must stay
indexable, gate only /api/.
Limits
Tokens are single-use and expire after 300 seconds. Gate One consumes them; the backend must not call Siteverify again.
The clearance cookie lasts
clearanceTtlSeconds(30 min by default); rotating the signing key invalidates every clearance.The client IP is sent to Cloudflare for validation; mention it in the privacy notice.
Cloudflare’s site key is bound to a domain list: every public hostname serving the widget must be declared there.
Try it
The source repository ships a runnable demo on *.localhost with
Cloudflare’s test keys:
cd deploy/local/turnstile
./run.sh gate # whole page gated
./run.sh api # only the action gated