
Cloudflare GatewayのForwardアクションで、許可したトラフィックにWorkerを挟む
どうも!オペ部の西村祐二です!
Cloudflare の Zero Trust Gateway で許可したトラフィックに独自の処理を挟みたいとき、これまでは NGINX などのプロキシを自前で立てて経路に差し込む必要がありました。アクセス理由の入力ページを出す、ロールに応じてヘッダーを足す、特定条件のアクセスを Slack に通知する、といった処理のためだけに、プロキシ群を運用する構成です。
changelog によると、2026年10月15日に public beta となった Forward アクションは、この処理を Cloudflare Worker に寄せる機能です。Gateway の HTTP ポリシーで許可したリクエストを Worker へ転送し、インラインのミドルウェアとして動かせます。この記事では、公式の changelog、ドキュメント、解説ブログ をもとに、Forward アクションでできること、Worker が受け取るもの、アーキテクチャ、課金と制限を整理します。あわせて、Worker 側のコードを手元で動かしたうえで、検証用の Zero Trust 組織で Gateway を通した通信まで試しました。
許可したトラフィックを Worker にプロキシする Forward アクション
Forward は、Gateway の HTTP ポリシーに追加された新しいアクションです。ポリシーの条件に一致して許可されたトラフィックを、指定した Worker へプロキシします。Worker はエンドユーザーとオリジンの間に入るインラインのミドルウェアとして動き、ヘッダーとリクエストボディを受け取って変更できます。オリジンへ転送したあと、返ってきたレスポンスを書き換えてからユーザーへ返すこともできます。
Worker は何を受け取り、何ができるか
Gateway は、Worker が export する GatewayMiddleware クラスの handle(request, context, next) メソッドを呼び出します。通常の fetch() ハンドラーではありません。
request: 元の HTTP リクエスト(ボディのストリームを含む)context: Gateway のコンテキスト。送信元 IP、ホスト、正規化された URL、ポリシー ID などのフィールドと、ユーザー情報を返すidentity()、デバイスの posture(状態)を返すdevicePosture()を持つnext(request): リクエストを元の宛先へ Gateway 経由で送り、レスポンスを返す。呼べるのは1回まで。宛先の URL やホストは変えられない
identity() と devicePosture() を使うと、「誰が」「どの端末から」のアクセスかを踏まえた処理を書けます。型定義は npm の @cloudflare/zt-types(2026.10.1 以降)で提供されています。
Worker が行える処理は、大きく次の3種類です。
- リクエスト/レスポンスの変換: ヘッダーやボディを書き換える。
- 独自ページの返却: 機微な宛先へのアクセス前に理由を入力させる interstitial(コーチング)ページを返す。
- 非同期の副作用: リクエストを変更せず、バックグラウンド処理だけを行う。例として Slack 通知、R2 への書き込み、外部パイプラインへのログ送信、Webhook 呼び出し、KV 更新が挙げられています。
処理の流れとアーキテクチャ
Forward を使っても、プロキシの土台は Gateway が引き続き担当します。公式の説明による流れは次のとおりです。
- TCP 接続、TLS の終端と再暗号化、証明書管理、HTTP バージョン交渉は Gateway が担当する。Worker は変換ロジックだけを書く。
- HTTP リクエストポリシーが、Forward ポリシーより先に評価される。
- Worker に渡す前に、元のリクエストボディを Gateway が検査する。ボディ検査でブロック判定が出ると、Worker の処理が中断されることがある。
- 最終的なレスポンスは、ユーザーに返す前に Gateway が検査する。
遅延の面では、Gateway と Workers が同じ Cloudflare ネットワークのマシン上で動くため、Worker へのディスパッチはローカル関数呼び出しになります。ネットワークのホップは発生せず、増える遅延は Worker の実行時間だけ、と説明されています。
対象プランと課金
changelog では、Forward アクションはすべての Zero Trust プランで利用できるとされています。解説ブログ側では、public beta の段階で Workers Free、Workers Paid、Workers for Enterprise のいずれでも機能制限なく使える、と記載されています。
Worker に送られたトラフィックは、通常の Worker リクエストとして課金されます。
設定の流れ
公式ドキュメント によると、前提と手順は次のとおりです。
- 前提: Gateway の HTTP フィルタリングを有効にして、テスト端末の通信を Gateway に通す。HTTPS を対象にするには TLS 復号を有効にし、端末にルート証明書を入れる。Worker と Gateway のポリシーは同じアカウントに置き、アカウントに workers.dev のサブドメインを設定しておく。
- Worker:
GatewayMiddlewareを export した Worker をデプロイする。公式の設定例はworkers_dev: falseとpreview_urls: falseで、Worker を公開 URL に出さない。 - ポリシー: Cloudflare One の Traffic controls > Firewall policies > HTTP で、アクションに Forward を選び、デプロイした Worker を指定する。API で作る場合は
action: "forward"とし、rule_settings.forward.worker_idに Worker の ID(32桁の16進数)を入れる。
ダッシュボードを日本語表示にしている場合、Forward は「転送」、Firewall policies は「ファイアウォール ポリシー」と表示されます。今回は API で作ったポリシーを編集画面で開きました。条件は Host が example.com のとき、アクションは「転送」で、Worker の欄で実行する Worker を選びます。

公式の例は、レスポンスに x-gateway-worker ヘッダーを足すだけのパススルーの Worker です。
import { WorkerEntrypoint } from "cloudflare:workers";
import type {
GatewayMiddlewareContext,
GatewayMiddlewareNext,
} from "@cloudflare/zt-types";
export class GatewayMiddleware extends WorkerEntrypoint {
async handle(
request: Request,
context: GatewayMiddlewareContext,
next: GatewayMiddlewareNext,
): Promise<Response> {
const response = await next(request);
const headers = new Headers(response.headers);
headers.set("x-gateway-worker", "gateway-worker");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
}
このほか、Slack 通知、R2 へのファイル保存、HTML への透かし、コーチングページ、JSON フィールドの伏せ字など7種類の Worker テンプレート が用意されています。
Worker 側を手元で試す
Gateway を通さずに確かめられる範囲として、wrangler 4.151.0 と @cloudflare/zt-types 2026.10.1 で次の3つを試しました。
デプロイと Worker ID
上のパススルーの Worker を、公式の設定(workers_dev: false、preview_urls: false)のまま Workers Free プランのアカウントへ wrangler deploy しました。fetch() ハンドラーも default export もないスクリプトですが、デプロイは成功しました。公開 URL を持たないため、出力は No targets deployed になります。List Workers API で取れた ID は、ポリシーに指定する32桁の16進数の形式でした。確認後に Worker は削除しています。
wrangler dev には default export が要る
同じスクリプトを wrangler dev で起動しようとすると、次のエラーでビルドが止まりました。
✘ [ERROR] No matching export in "src/index.ts" for import "default"
Gateway は GatewayMiddleware しか呼ばないので、デプロイに default export は要りません。一方で、wrangler dev は default export を前提にスクリプトを組み立てます。そこで、ローカル専用の入口ファイルを用意し、wrangler.jsonc の main だけを src/dev.ts に変えた wrangler.dev.jsonc で起動しました。
// ローカル専用の入口。Gateway が呼ぶのは GatewayMiddleware だけ
export { GatewayMiddleware } from "./index.ts";
export default { fetch: () => new Response("not used", { status: 404 }) };
Gateway の呼び出し方を真似て handle() を呼ぶ
Gateway は Workers RPC で handle() を呼び出します。そこで、名前付きエントリポイントへの Service Binding を持つテスト用の Worker を作り、偽の context と偽の next を渡して呼び出しました。next は、呼ばれた回数を数えて固定のレスポンスを返すだけの関数です。このテスト用 Worker は Gateway の呼び出し方を真似ただけで、Gateway そのものではありません。
呼び出す相手は2つです。1つは上のパススルーの Worker(gw/)です。もう1つは Available context の例(posture/)で、メールアドレスと特定の posture チェックを求め、取得に失敗したら 503 を返します。どちらにも上と同じ形のローカル専用の入口を足し、テスト用 Worker(sim/)から Service Binding で呼びます。
{
"name": "gateway-sim",
"main": "src/index.ts",
"compatibility_date": "2026-10-16",
"services": [
{ "binding": "PASS", "service": "gateway-forward-sample", "entrypoint": "GatewayMiddleware" },
{ "binding": "POSTURE", "service": "gateway-posture-sample", "entrypoint": "GatewayMiddleware" }
]
}
const res: Response = await env.PASS.handle(
new Request("https://example.com/hello"),
{
srcIp: "198.51.100.10", srcPort: 50000, networkProtocol: "tcp",
accountId: "<account-id>", policyId: "00000000-0000-0000-0000-000000000000",
host: "example.com", uri: "https://example.com/",
identity: async () => ({ email: "user@example.com" }),
devicePosture: async () => ({ checks: { passed: [] } }),
},
next,
);
3つの Worker の設定ファイルを並べて wrangler dev に渡すと、1つのプロセスで動きます。
npx wrangler dev -c sim/wrangler.jsonc -c gw/wrangler.dev.jsonc -c posture/wrangler.dev.jsonc
context の中身を変えた6パターンを、テスト用 Worker を呼んで結果を JSONL に書き出すスクリプト(local-sim.sh)で実行しました。結果は次のとおりです。

| パターン | ステータス | next の呼び出し |
|---|---|---|
| パススルー | 200(x-gateway-worker 付き) |
1回 |
| posture の例: メールあり、チェック通過 | 200 | 1回 |
posture の例: 匿名(identity() が null) |
403 | 0回 |
| posture の例: 必要なチェックが未通過 | 403 | 0回 |
posture の例: identity() が例外 |
503 | 0回 |
posture の例: devicePosture() が例外 |
503 | 0回 |
拒否するパターンでは next が呼ばれず、元の宛先へのリクエストは発生しません。ドキュメントによると、identity() や devicePosture() の例外を捕まえないと Gateway は 500 を返します。この例のように try/catch で受け止め、どのステータスを返すかを自分で決めておくことになります。
Gateway を通して試す
続いて、実際に Gateway を通した通信で動かしました。準備したものは次のとおりです。
- 端末: Linux のコンテナに Cloudflare One Client を入れ、サービストークンで登録 した。コンテナの中だけで完結するので、手元の PC の通信経路や証明書ストアは変わらない。
- Gateway: ルート証明書を作成して有効にし、TLS 復号をオンにした。証明書の公開部分をコンテナの信頼ストアに入れた。
- Worker: 公式のパススルーの例に、受け取った
contextの要約をx-gateway-contextヘッダーで返す処理を足したもの - ポリシー:
http.request.host == "example.com"を条件に Forward する HTTP ポリシーを API で作った。
構成を図にすると次のとおりです。図は筆者が作成した概念図で、公式の構成図ではありません。コンテナからの通信は Cloudflare One Client で Gateway に送られ、TLS 復号と HTTP ポリシーの評価を経て、Forward ポリシーに一致した example.com だけが Worker に渡ります。Worker は next() で、Gateway 経由で元の宛先へリクエストを送ります。

TLS 復号は、ダッシュボードの Traffic controls > Traffic settings(日本語表示では「トラフィックの設定」)でオンになっていることを確認できます。

登録できた端末は、ダッシュボードのデバイス一覧に表示されます。サービストークンで登録したため、メールアドレスは non_identity@<team-name>.cloudflareaccess.com です。

HTTP ポリシーの一覧には、作ったポリシーがアクション「転送」で並びます。

ポリシーに一致したリクエストだけ Worker を通る
コンテナから、ポリシーの対象の example.com と、対象外の example.org にリクエストを送りました。
$ curl -sS -o /dev/null -D - https://example.com/
HTTP/2 200
x-gateway-context: {"keys":["accountId","devicePosture","domainCategories","dstIp","dstPort","host","identity","internalSrcIp","networkProtocol","onrampType","policyId","sni","srcIp","srcPort","uri","vnetId"],"host":"example.com","uri":"https://example.com/","requestUrl":"https://example.com/","networkProtocol":"tcp","onrampType":"device_client","sni":"example.com","dstPort":443,"domainCategories":2,"identity":"object(email:non_identity@<team-name>.cloudflareaccess.com, groups:0)","posture":"passed:0"}
x-gateway-worker: gateway-worker
$ curl -sS -o /dev/null -D - https://example.org/
HTTP/2 200
example.com のレスポンスにだけ Worker が付けたヘッダーがあり、example.org は Worker を通らずに返っています。x-gateway-context は、受け取った値を Worker 側で要約したもので、配列は件数にしています(domainCategories は2件)。証明書の発行者が「Gateway CA - Cloudflare Managed G1」であることから、TLS が Gateway で復号されていることも確かめました。
context に入っていたのは、ドキュメントの表にある16個のフィールドのうち14個と、identity()・devicePosture() です。proxyEndpoint と embeddedProtocols はありませんでした。接続方法を表す onrampType は device_client です。
サービストークンで登録した端末の identity()
ドキュメントでは、identity() は識別されたユーザーがいなければ null を返すとされています。ところが、サービストークンで登録した端末では null ではなく、メールアドレスが non_identity@<team-name>.cloudflareaccess.com のオブジェクトが返りました(グループは0件)。
手元で試した節で動かした公式の posture の例は、「メールアドレスがなければ 403」という判定です。そのため、この端末からのアクセスは、メールアドレスの有無の判定では弾かれません。サービストークンで登録した端末を通したくない場合は、メールアドレスの有無ではなく、許可するドメインやグループで判定するのがよさそうです。ドキュメントによると Forward ポリシーの条件にも ID や posture の条件を加えられるので、こうした端末をポリシー側で対象から外す方法もあります。
ログでの見え方
Gateway の HTTP リクエストログでは、Forward ポリシーに一致したリクエストのアクションは「許可」と表示されました。ポリシー名の欄に Forward ポリシーの名前が入るので、どのリクエストが Worker を通ったかはこの欄で見分けます。

行を開いて詳細を見ても、アクションは allow で、一致したポリシーに Forward ポリシーの名前が出るだけでした。JSON 表示にも、Worker を通ったことを示す項目はありませんでした。

想定ユースケース
解説ブログで挙げられている使いどころを整理すると、次のようになります。
| ユースケース | 概要 |
|---|---|
| 理由入力ページ(interstitial) | 機微な宛先へのアクセス前に理由を入力させ、KV で TTL 付きセッションを管理し、D1 や Hyperdrive 経由で監査ログを残す |
| 動的ヘッダー注入 | JWT からロールを取り出し、上流リクエストに X-App-Role のようなヘッダーを設定する(解説ブログでは、ロールの取り出しを後述の Custom Functions で行う例) |
| ファイル検査とログ保存 | 条件に合うファイルを外部サンドボックスへ送る、または R2 に保存する |
| 一致時のリアルタイム通知 | ポリシーに一致したときに、ユーザー・ルール・宛先 URL を Slack や PagerDuty へ送る |
なお、解説ブログは通知の例として DLP の一致も挙げています。ただしドキュメントの制限事項では、DLP の一致結果やボディ検査の結果は Forward の条件に使えないとされています。
同時に発表された関連機能
Forward アクションは、Gateway に Worker のロジックを持ち込む流れの一部として発表されました。同じ解説ブログでは、次の2つも紹介されています。
- Custom Functions: ポリシー評価の最中に Worker を呼び出し、文字列・数値・真偽値を返してポリシービルダーのセレクターとして使う機能。「data in, value out」の設計で、短時間で終わるステートレスな評価向け。解説ブログでは2026年末までの提供予定とされています。
- DLP custom validation: DLP の検出が一致した後に Worker で検証ロジックを走らせ、判定を補正したり、取り消し(veto)たりできる機能。正規表現だけでは分からない「そのデータが実際に有効か」を自社の情報源で確認できます。解説ブログの公開時点で提供が始まっています。
Forward がユーザーのライブリクエストへのインライン変換に向くのに対し、Custom Functions は短時間のステートレスな評価に向く、という使い分けが示されています。Worker のロジックを持ち込める対象は現時点では Gateway と DLP で、次の対象として Email Security が予定されています。
注意点・制限
- Workers Free には実行時間の上限と1日のリクエスト上限があります(Workers の制限)。Gateway のトラフィックを流すと Worker のリクエストが増えるため、Free で使う場合は上限に注意が必要です。
- Forward で使う Worker は、エンドユーザーのライブリクエストに割り込みます。体験への影響を抑えるため、対象を絞って慎重に設計する必要があると、解説ブログに書かれています。
- Worker の呼び出しや通信に失敗したときは fail closed です。Gateway が Worker を飛ばして元のリクエストを送ることはありません。
- WebSocket、HTTP の CONNECT トンネル、プロトコルのアップグレード、HTTP trailers、Do Not Inspect に一致する通信は Worker を通りません。また、Workers VPC から出る HTTP 通信が Forward ポリシーに一致すると、Gateway は Worker を呼ばずに破棄します。
- Forward ポリシーが参照している Worker を削除しても、ポリシーは残ります。その後の一致するリクエストは HTTP 500 になるため、Worker を消す前にポリシーを付け替えるか削除しておく必要があります。
next()は元の宛先にしか送れません。Worker 内の通常のfetch()は独立したサブリクエストになり、ユーザーの Gateway ポリシーや egress IP は使われません。- Forward アクションは public beta の段階です。beta の終了時期は changelog・ドキュメント・解説ブログのいずれにも書かれていません。
まとめ
Cloudflare Gateway の Forward アクションは、HTTP ポリシーで許可したトラフィックを Worker へプロキシし、インラインのミドルウェアとしてリクエスト/レスポンスの変換や非同期処理を行える機能です。Gateway と Workers が同一マシンで動くため、ディスパッチはローカル関数呼び出しになり、外部プロキシのような往復が増えない点が特徴です。public beta の段階で全 Zero Trust プランで使え、Worker トラフィックは通常の Worker リクエストとして課金されます。
実際に Gateway 経由で試すと、ポリシーに一致したリクエストだけが Worker を通り、context には接続情報や登録端末の情報が入っていました。ログ上のアクションは「許可」のままなので、Worker を通ったかはポリシー名で見分けます。サービストークンで登録した端末でも identity() がメールアドレス付きのオブジェクトを返すので、身元の判定はメールアドレスの有無だけに頼らないほうが安全です。ローカルで試すときは、wrangler dev 用の default export と、handle() を呼び出すテスト用の Worker を足すと、Gateway なしでも分岐を確かめられます。失敗時は fail closed で、Worker を削除すると一致したリクエストが 500 になる点には注意が必要です。
誰かの参考になれば幸いです。
参考リンク:







