- エッジで守るという発想 — リクエストがアプリに届く前に検査・遮断する多層防御
- Turnstile(CAPTCHA 代替)のウィジェット設置と、Siteverify API による Worker でのトークン検証
- WAF の 3 本柱(マネージドルール・カスタムルール・レート制限ルール)と Bot 対策の全体像
- Workers の secrets の安全な扱いと、アカウント全体で再利用できる Secrets Store
- 無料プランでどこまでできるか(2026-08 時点の制限値つき)
エッジで守るという発想
Cloudflare の定石はリクエストがアプリに到達する前に、エッジ(Cloudflare のネットワーク)で検査・遮断することです。攻撃リクエストが Worker やオリジンサーバーの CPU を消費する前に落とせます。守りは役割の異なる複数の層で構成されます。
- Turnstile: フォーム単位の「人間確認」。従来型 CAPTCHA の置き換え
- WAF: HTTP リクエスト全般のフィルタリング(既知の攻撃シグネチャ + 自作ルール)
- レート制限: リクエストの「量」を制御し、ブルートフォースやスクレイピングを抑止
- Bot 対策: 自動化されたトラフィックの検知と対処
WAF の役割は検出(detection)と緩和(mitigation)の 2 段階に分かれています。攻撃スコアや bot スコアなどの検出機能はリクエストに「点数を付けるだけ」で、それ自体はブロックしません。スコアを式で参照するカスタムルールやレート制限ルールを作って初めて緩和が実行されます。
ルールは種類ごとに決まった順序で評価され、Block や Managed Challenge のような終端アクション(terminating action)にマッチした時点で以降の評価は止まります。主要なものの実行順は次のとおりです。
- IP Access ルール
- カスタムルール
- レート制限ルール
- マネージドルール
適用単位にも重要な違いがあります。WAF・レート制限は「ゾーン」(Cloudflare に追加した独自ドメイン)単位の機能です。一方 Turnstile はゾーンに依存しない独立サービスで、Cloudflare を経由しないサイトや workers.dev 上のアプリにもそのまま使えます。ハンズオンが Turnstile 中心なのはこのためです(ゾーンの作り方は次章)。
Turnstile — CAPTCHA 代替の仕組み
Turnstile は「歪んだ文字を読む」タイプの従来型 CAPTCHA を置き換える、Cloudflare のスマート CAPTCHA 代替です。仕組みは 2 段構えで、ブラウザ側の JavaScript ウィジェットがチャレンジを実行してトークンを発行し、サーバー側がそのトークンを Siteverify API に送って正当性を確認します。
ウィジェットを作成すると、キーのペアが発行されます。
| キー | 役割 | 扱い |
|---|---|---|
| sitekey | ページ側でウィジェットを呼び出す公開キー | HTML に埋め込んでよい |
| secret key | サーバー側でトークンを検証する秘密キー | クライアントに絶対に出さない |
ウィジェットには 3 つのモードがあります。
| モード | 見え方 | 特徴 |
|---|---|---|
| Managed(推奨) | 必要時のみチェックボックス | リスクに応じて対話の要否を Cloudflare が自動判断。読解問題は出ない |
| Non-Interactive | スピナー表示のみ | 訪問者は一切操作しない |
| Invisible | 完全に非表示 | 操作も表示もなし。自サイトのプライバシーポリシーで Turnstile Privacy Addendum への言及が必要 |
ページへの設置は「implicit レンダリング」なら 2 行で済みます。スクリプトを読み込み、ウィジェットを出したい場所に cf-turnstile クラスの div を置くだけです。
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form action="/submit" method="POST">
<input type="email" name="email" required />
<div class="cf-turnstile" data-sitekey="<YOUR-SITE-KEY>"></div>
<button type="submit">送信</button>
</form>
form 内に置くと、チャレンジ完了時に cf-turnstile-response という不可視 input が自動追加され、トークンが他のフィールドと一緒に送信されます。SPA など動的なページでは ?render=explicit を付けて turnstile.render() を自分で呼ぶ explicit レンダリングを使います。
Siteverify API によるサーバー側検証
サーバー側検証は必須です。ウィジェットの設置だけでは何も守れません。理由は 3 つあります。
- トークンは偽造できる: 攻撃者はチャレンジを完了せずに任意の文字列をフォームエンドポイントへ直接 POST できる
- トークンは 300 秒(5 分)で失効する
- トークンは 1 回しか検証できない: 再利用は
timeout-or-duplicateエラーで拒否される
検証は https://challenges.cloudflare.com/turnstile/v0/siteverify への POST で行います。リクエストは form-urlencoded と JSON の両方を受け付け、レスポンスは常に JSON です。
| パラメータ | 必須 | 内容 |
|---|---|---|
secret | Yes | ウィジェットの secret key |
response | Yes | クライアントから受け取ったトークン(最大 2,048 文字) |
remoteip | No | 訪問者の IP アドレス(Worker では CF-Connecting-IP ヘッダーから取得) |
idempotency_key | No | リトライを安全にするための UUID |
レスポンスの success が true のときだけフォームを処理します。hostname(チャレンジが実行されたホスト名)や action(ウィジェットの data-action 値)が期待どおりかも確認すると、トークンの流用をより厳密に防げます。
開発時にはテスト専用キーが用意されており、実際のチャレンジを起こさずに成功・失敗・重複の各パターンを再現できます。テスト sitekey は localhost を含む任意のドメインで動作します。
| キー | 種別 | 挙動 |
|---|---|---|
1x00000000000000000000AA | sitekey | 常に成功(可視) |
2x00000000000000000000AB | sitekey | 常に失敗(可視) |
3x00000000000000000000FF | sitekey | 強制的に対話チャレンジ |
1x0000000000000000000000000000000AA | secret key | 検証が常に成功 |
2x0000000000000000000000000000000AA | secret key | 検証が常に失敗 |
3x0000000000000000000000000000000AA | secret key | 常に「使用済みトークン」エラー |
Turnstile は無料プランで実用十分です。2026-08 時点で、無料枠はウィジェット 20 個まで・1 ウィジェットあたりホスト名 10 件まで・チャレンジ回数は無制限です(Enterprise はブランド非表示や Ephemeral ID などが追加)。
WAF — マネージドルール・カスタムルール・レート制限
WAF はゾーンに入ってくる Web・API リクエストを Rules language(式言語)で評価し、望ましくないトラフィックをフィルタリングします。構成要素は 3 本柱です。
マネージドルール — 既製の防御セット
Cloudflare が作成・毎週更新する既製ルールセットで、有効化するだけで既知の攻撃手法やゼロデイ脆弱性への防御が得られます。無料プランでは Free Managed Ruleset(影響の大きい脆弱性に絞ったサブセット)がデフォルトで適用済みです。Pro 以上では、より広範な Cloudflare Managed Ruleset と、スコア累積方式の OWASP Core Ruleset を有効化できます。OWASP はマッチした各ルールのスコアを合算し、しきい値超過でアクションを実行する方式で、誤検知(false positive)が出やすく、しきい値と paranoia level の調整が前提です。
カスタムルール — 自分の条件で守る
「この条件にマッチしたら Block / Managed Challenge / Skip」という自作ルールです。式には IP・国・パス・ヘッダー・ボディなどのリクエストプロパティを使えます。たとえば管理画面を特定 IP 以外から遮断するルールは次のような式になります。
(http.request.uri.path contains "/admin" and not ip.src in {203.0.113.7 198.51.100.0/24})
アクションに Block を選べば、この式にマッチしたリクエストはエッジで 403 相当の応答を受け、Worker にもオリジンにも届きません。
レート制限ルール — 量で守る
「同一 IP から 10 秒間に 10 回を超えて /login に POST が来たらブロック」のように、式 + 期間 + 回数 + 超過時のアクションで定義します。ほかに回数のカウント特性(characteristics。既定は IP)と、超過後にアクションを維持する時間(mitigation timeout)を指定します。カウンターはデータセンター単位で、反映に数秒の遅延があるため、「厳密に N 回まで許可」ではなく「異常な量を面で止める」装置と捉えるのが正しい理解です。
ログイン保護の定石は、閾値の異なる複数ルールの段階構成です。公式ベストプラクティスでは「1 分に 4 回失敗で Managed Challenge」「10 分に 10 回で Managed Challenge」「1 時間に 20 回で 1 日ブロック」という 3 本のルールを同時に並べる構成が紹介されています(3 本ともカウント対象は同じで、窓・しきい値・アクションだけが異なります)。Business 以上ではカウント式を分離でき、「401/403 を返したリクエストだけカウントする」応答ステータス連動も可能です。
Bot 対策
Cloudflare はリクエストごとに bot スコア(1〜99。1 が「ほぼ確実に bot」、99 が「ほぼ確実に人間」)を算出します。無料プランはドメイン全体で検知 bot にチャレンジを出す Bot Fight Mode、Pro 以上はカテゴリ別にアクションを設定できる Super Bot Fight Mode、Enterprise は bot スコアそのものをカスタムルールの式で使える Bot Management です。細かいスコア値の利用は Enterprise 限定です。
主要機能のプラン別可否をまとめます(2026-08 時点)。
| 機能 | Free | Pro | Business | Enterprise |
|---|---|---|---|---|
| マネージドルール | Free Managed Ruleset のみ | あり | あり | あり |
| カスタムルール数 | 5 | 20 | 100 | 1,000 |
| カスタムルールの正規表現 | 不可 | 不可 | 可 | 可 |
| レート制限ルール数 | 1 | 2 | 5 | 100 |
| レート制限の期間 | 10 秒のみ | 最大 1 分 | 最大 10 分 | 最大 65,535 秒 |
| Bot 対策 | Bot Fight Mode | Super Bot Fight Mode | Super Bot Fight Mode | Bot Management(bot スコア) |
secrets の扱いと Secrets Store
Turnstile の secret key のような機微情報を、Worker ではどう保管すべきでしょうか。答えは secrets(暗号化されたテキスト値のバインディング)です。wrangler.jsonc の vars は平文でダッシュボードや Wrangler から見えてしまうため、機微情報を vars に書いてはいけません。
secrets は 2 系統あります。
- Worker 単位の secrets:
npx wrangler secret put <KEY>で登録。設定後は値を誰も閲覧できず、コードからは通常の環境変数と同じくenv.KEYで読める - Secrets Store(2026-08 時点 open beta): アカウントレベルの集中管理ストア。1 つの secret を複数の Worker や AI Gateway から再利用でき、ロールベースのアクセス制御と監査ログを備える
env.KEY で読む)は完全に同じなので、vars から secrets への移行コストはほぼゼロです。ローカル開発では、プロジェクト直下の .dev.vars ファイル(dotenv 形式)の値が env に流れます。.dev.vars と .env は必ず .gitignore に追加してください。本番の登録は次のとおりです。
npx wrangler secret put TURNSTILE_SECRET_KEY
# プロンプトが出るので値を貼り付ける
# → 新しいバージョンが作成され、即座にデプロイされる
wrangler secret put はその場で新バージョンを作って即デプロイする点に注意してください(段階デプロイ中なら wrangler versions secret put を使います)。CI/CD ではコードと secrets を同時に上げる npx wrangler deploy --secrets-file .env.production も使えます。
Secrets Store を使う場合は、ストアに secret を作成してから wrangler.jsonc でバインドします。
{
"secrets_store_secrets": [
{
"binding": "TURNSTILE_SECRET",
"store_id": "<STORE_ID>",
"secret_name": "turnstile-secret-key"
}
]
}
<STORE_ID> はダッシュボードの Secrets Store タブか npx wrangler secrets-store store list で確認できる ID に置き換えます。Worker 単位の secrets と違い、値の取得は const key = await env.TURNSTILE_SECRET.get() と非同期呼び出しになります。この章のハンズオンは、どのプランでも使える Worker 単位の secrets で進めます。
お問い合わせフォームを配信し、Siteverify API でトークンを検証する Worker を作ります。テストキーを使うので Turnstile ウィジェットの作成は不要、すべてローカルで完結します。所要 20 分。
このハンズオンは無料プランで完結します。テストキーは任意のドメイン(localhost 含む)で動作します。
- プロジェクトを作成する(テンプレートは Hello World / TypeScript を選択)
npm create cloudflare@latest turnstile-form cd turnstile-form src/index.tsを次の内容に置き換える。まずフォームの HTML 定数からconst PAGE = `<!doctype html> <html lang="ja"> <head> <meta charset="utf-8" /> <title>お問い合わせ</title> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> </head> <body> <form action="/submit" method="POST"> <input type="email" name="email" placeholder="メールアドレス" required /> <div class="cf-turnstile" data-sitekey="1x00000000000000000000AA"></div> <button type="submit">送信</button> </form> </body> </html>`;- 続けて、同じファイルに fetch ハンドラーと検証関数を書く
interface Env { TURNSTILE_SECRET_KEY: string; } interface SiteverifyResult { success: boolean; hostname?: string; "error-codes"?: string[]; } async function verifyToken(token: string, remoteip: string, env: Env) { const res = await fetch( "https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ secret: env.TURNSTILE_SECRET_KEY, response: token, remoteip, }), }, ); return (await res.json()) as SiteverifyResult; } export default { async fetch(request, env): Promise<Response> { const url = new URL(request.url); if (request.method === "GET" && url.pathname === "/") { return new Response(PAGE, { headers: { "Content-Type": "text/html; charset=utf-8" }, }); } if (request.method === "POST" && url.pathname === "/submit") { const form = await request.formData(); const token = String(form.get("cf-turnstile-response") ?? ""); const ip = request.headers.get("CF-Connecting-IP") ?? ""; const result = await verifyToken(token, ip, env); if (!result.success) { const codes = (result["error-codes"] ?? []).join(", "); return new Response(`検証に失敗しました: ${codes}`, { status: 400 }); } return new Response(`送信を受け付けました (hostname: ${result.hostname})`); } return new Response("Not Found", { status: 404 }); }, } satisfies ExportedHandler<Env>; - プロジェクト直下に
.dev.varsを作成し、テスト用 secret key を書く(このファイルを.gitignoreに追加)TURNSTILE_SECRET_KEY="1x0000000000000000000000000000000AA" npx wrangler devを起動し、ブラウザでhttp://localhost:8787を開く。フォーム内にテストウィジェットが表示され、すぐに成功状態になる- メールアドレスを入れて送信する。
送信を受け付けました (hostname: ...)と表示されれば、トークンが Siteverify API で検証されている - 失敗系も確認する。
.dev.varsの値を2x0000000000000000000000000000000AA(常に失敗する secret key)に変えて dev サーバーを再起動し、再送信すると検証に失敗しました: invalid-input-responseが返る
ウィジェットが発行したトークンがフォームの cf-turnstile-response フィールド経由で Worker に届き、サーバー側で検証されるまでの流れを一気通貫で確認できました。
14-1 の Worker を、本物のウィジェットと wrangler secret を使って公開します。所要 15 分。
このハンズオンも無料プランで完結します(Turnstile 無料枠: ウィジェット 20 個、2026-08 時点)。
- Cloudflare ダッシュボードで Turnstile ページを開き、ウィジェットを作成する。名前は「Contact Form」など用途がわかるものにし、モードは Managed、ホスト名に
turnstile-form.<YOUR_SUBDOMAIN>.workers.dev(自分の workers.dev サブドメイン)を追加する - 発行された sitekey と secret key を控える。secret key はこの後 secrets に入れるのでコードに貼らない
src/index.tsのdata-sitekeyをテストキーから本番 sitekey に置き換える- デプロイする
npx wrangler deploy - 本番の secret key を登録する。プロンプトに値を貼り付けると、新バージョンが作られて即デプロイされる
npx wrangler secret put TURNSTILE_SECRET_KEY https://turnstile-form.<YOUR_SUBDOMAIN>.workers.devを開き、フォームを送信する。今度は本物のチャレンジが実行され、検証成功のメッセージが返る
ダッシュボードの Turnstile ページでチャレンジ数・トークン検証数のアナリティクスを確認できます(無料プランは過去 7 日分・2026-08 時点)。Siteverify を呼んでいないウィジェットは検証数が 0 のままなので、実装漏れの検知にも使えます。
つまずきポイント
テストキーと本番キーを混ぜると必ず失敗する — テスト sitekey が発行するのはダミートークン(XXXX.DUMMY.TOKEN.XXXX)で、本番 secret key はこれを拒否します。逆に、テスト secret key は本物のトークンを拒否します。sitekey と secret key は必ず「テスト同士」「本番同士」の組で使ってください。
timeout-or-duplicate エラーが出る — トークンは発行から 300 秒で失効し、1 回しか検証できません。フォームを開いたまま放置して送信した場合や、同じトークンを 2 回検証した場合に発生します。クライアント側で turnstile.reset() を呼んでウィジェットをリセットし、新しいトークンを取得させてください。
api.js を自分のサーバーから配信してはいけない — Turnstile のスクリプトは必ず challenges.cloudflare.com の正規 URL から読み込みます。プロキシやキャッシュ配信をすると Cloudflare 側の更新時に壊れます。またウィジェットは http:// / https:// のページ専用で、file:// で開いた HTML では動きません。CSP 利用時は challenges.cloudflare.com への接続許可が必要です。
secret を vars に書いてしまう / .dev.vars をコミットしてしまう — wrangler.jsonc の vars は平文で閲覧可能なので、機微情報は必ず wrangler secret put で登録します。ローカル用の .dev.vars と .env は .gitignore に追加してください。なお両方のファイルがあると .dev.vars だけが読まれ、.env は無視されます(併用不可)。
理解度チェック
Q1. Turnstile のウィジェットを設置するだけでは不十分で、Siteverify API の呼び出しが必須なのはなぜ?
トークンは偽造できるからです。攻撃者はチャレンジを完了せず任意の文字列をフォームエンドポイントへ直接 POST できるため、サーバー側で secret key を使って Siteverify API に問い合わせ、トークンが本物か・失効していないか・未使用かを確認して初めて防御が成立します。
Q2. Turnstile トークンの有効期間と再利用の可否は?
発行から 300 秒(5 分)で失効し、検証は 1 回限りです。期限切れ・使用済みトークンの検証は timeout-or-duplicate エラーになり、turnstile.reset() で新しいトークンを取得し直す必要があります。
Q3. WAF のカスタムルールとマネージドルール、先に評価されるのはどちら?
カスタムルールです。実行順は IP Access ルール → カスタムルール → レート制限ルール → マネージドルールで、先に Block などの終端アクションにマッチした時点で以降の評価は止まります。
Q4. 無料プランのレート制限ルールで設定できるルール数とカウント期間は?(2026-08 時点)
1 ルールのみ、カウント期間・ブロック継続時間ともに 10 秒固定です。カウント特性は IP、式に使えるフィールドは Path と Verified Bot に限られます。より長い期間や複数ルールには有料プランが必要です。
Q5. Turnstile の secret key を Worker で保管する場所として vars が不適切な理由と、正しい方法は?
vars は平文でダッシュボードや Wrangler から値が見えてしまうためです。npx wrangler secret put で secrets として登録すれば設定後は値を閲覧できません(コードからはどちらも env 経由で読めます)。複数の Worker での再利用にはアカウントレベルの Secrets Store を使います。
まとめと次章へ
この章では、エッジでアプリを守る Cloudflare の多層防御を学びました。
- 防御は Turnstile(人間確認)・WAF(リクエスト検査)・レート制限(量の制御)・Bot 対策(自動化検知)の多層構成。検出はスコア付与のみで、緩和はルールで行う
- Turnstile はウィジェット(sitekey)+ Siteverify 検証(secret key)の 2 段構え。サーバー側検証は必須で、トークンは 300 秒・1 回限り
- テストキーを使えばローカルで成功・失敗・重複の全パターンを再現できる
- WAF はゾーン単位の機能。無料プランでも Free Managed Ruleset(デフォルト適用)・カスタムルール 5 個・レート制限 1 個が使える
- 機微情報は
varsではなく secrets へ。複数 Worker での再利用にはアカウントレベルの Secrets Store(open beta)
WAF やレート制限をフルに使うには、独自ドメインを Cloudflare のゾーンとして追加する必要があります。次章「CDN・DNS・カスタムドメイン」では、そのゾーンの作り方 — DNS 管理、プロキシ、キャッシュ、Worker へのカスタムドメイン割り当て — を学びます。