Cloudflare Ruleset を API で把握する(cloudflare-typescript と JSON Crack)
はじめに
Cloudflare のプロダクトを見ると Ruleset Engine を利用するものが増殖しています。
この記事ではルールセットを API で把握する例を示し、運用に活かすことを考えます。
Google 翻訳
基本用語
-
Phases
- リクエストやレスポンスに対する複数の処理フェーズ( WAF や Rate Limit など)
- 各フェーズにルールセットを定義(アカウントレベルあるいはゾーンレベル)
- アカウントレベルはエンタープライズプランのみ
- アカウントレベルがある場合、先に評価
-
Rulesets
- それぞれのフェーズで実行するルールの塊
-
Rules
- リクエストやレスポンスを評価する個々のルール
リクエスト・レスポンスはフェーズごとのルールセット内のルールで評価される
ルールセットの使われ領域
ダッシュボードではルールセットを利用しているプロダクト機能(フェーズ)確認することができます。
この記事ではそれぞれのルールセットの箱の中を API 確認する方法を書きます。
-
トラフィックシーケンス
フェーズとその有効化状況 -
トレース
宛先 URL を指定し、どのルールが適用されるかシミュレート
API での確認に使うツール
今回の確認には下記を利用しました。
- ライブラリ
コードで Cloudflare と戯れる の中から、cloudflare-typescript
を使ってみます。
-
サンプルコード
API Dev docs のTypeScript
を選べばcloudflare-typescript
のサンプルを確認できます。
-
可視化
API を叩いた結果の JSON を見やすくするために、JSON Crack
を使います。
VS Code の extension があったので、使いました。
実践
まずエントリーポイントから始めよ
ルールセットの確認でまず重要なのはフェーズのエントリーポイントルールセットです。
下記の役割があります。
- そのフェーズにリクエストやレスポンスを引き込む
- マッチ評価を行い、アクション
- ルールの適用(
log
など)や他のルールセットの呼び出し(execute
)
- ルールの適用(
- 次のルール・フェーズに渡す(
Termintaing Actions? が No
のアクションの場合)
図のオレンジ枠の部分です。
フェーズ(エントリーポイント)ルールセット・管理ルールセット・ルール
参考:ダッシュボードで WAF 管理ルールセットを有効にしたときの裏側
ダッシュボードで WAF 管理ルールセットをデプロイ
↓
当該のフェーズ(エントリーポイント)ルールセットが新規作成される
↓
エントリーポイントルールセットからデフォルトの WAF 管理ルールセットが呼ばれる
↓
エントリーポイントルールセットにデフォルトからの上書き設定があれば、対象のルールが上書きされる
↓
ルールがトラフィックの評価に適用される
このように、エントリーポイントルールセットができることで、そのフェーズでルールが適用されます。
そのため、まずはこの情報を取るところから始めます。
エントリーポイントルールセットの見分け方
各フェーズのエントリーポイントルールセットが有効かどうかは、ルールセット JSON オブジェクトの kind
フィールドを見ればよいです。
- kind:
root
アカウントレベルのエントリーポイント - kind:
zone
ゾーンレベルのエントリーポイント
エントリーポイントルールセットの kind
kind 一覧
アカウントレベル
アカウントレベルのエントリーポイントルールセットを全て取得します。
cloudflare-typescript のサンプルは下記のとおりです。
rulesets.get.account.phase.entry.ts
Cloudflare.rulesets.list
Cloudflare.rulesets.phases.get
const Cloudflare = require('cloudflare')
const dotenv = require('dotenv')
dotenv.config()
const cloudflare = new Cloudflare({
apiToken: process.env['CLOUDFLARE_TOKEN'],
})
async function main() {
const rulesetsList = await cloudflare.rulesets.list({
account_id: process.env['ACCOUNT_ID']
})
// 1. Entry point ruleset のみを抽出
const entryRulesets = rulesetsList.result.filter(ruleset => ruleset.kind === 'root')
// 2. phase を取り出し、新しいオブジェクト
let entryPhases = entryRulesets.map(ruleset => ruleset.phase)
// 3. 念のため重複排除
entryPhases = [...new Set(entryPhases)]
// 4. phase ごとに ruleset を取得
entryPhases.forEach(async entryPhase => {
const ruleset = await cloudflare.rulesets.phases.get(
entryPhase,
{
account_id: process.env['ACCOUNT_ID']
}).catch(async (err) => {
if (err instanceof Cloudflare.APIError) {
console.log(err.status)
} else {
throw err
}
})
console.log(JSON.stringify(ruleset))
})
}
main()
このアカウントでは以下の複数のフェーズが有効になっていました。
[
'http_request_firewall_managed',
'magic_transit',
'magic_transit_ids_managed',
'magic_transit_managed',
'http_request_firewall_custom'
]
この中から http_request_firewall_managed
WAF 管理ルールセットのフェーズを掘ってみます。
ダッシュボード
まず WAF 管理ルールセット
のダッシュボードを見てみます。
アカウント > WAF > 管理ルールセット
この画面が http_request_firewall_managed
のエントリーポイントルールセット と言えます。
エントリーポイントルールセット設定内容
下記のルール・ルールセットで構成されています。
-
特定パス
は WAF 管理ルールセットをスキップ
(無効) -
ホストごとに個別
の WAF 管理ルールセットを実行
(有効)
API
次に API でエントリーポイントルールセットを取ってみます。
node --no-warnings rulesets.get.account.phase.entry.ts \
| jq -s '.[]|select(.phase=="http_request_firewall_managed")'
前述のコードを実行し、http_request_firewall_managed
だけを抜き出しています。
出力
API の出力にはダッシュボードより一階層深い内容も含まれます。
ダッシュボードの場合はクリックで進んでいけば見れる情報です。
{
"description": "",
"id": "0fea3f5a773d4202baeb89c705a2e74a",
"kind": "root",
"last_updated": "2025-03-30T03:08:55.422644Z",
"name": "root",
"phase": "http_request_firewall_managed",
"rules": [
{
"action": "skip",
"action_parameters": {
"ruleset": "current"
},
"description": "スキップ",
"enabled": false,
"expression": "(http.request.uri.path wildcard \"/skip/*\")",
"id": "ebadd8bb68d14db088bc40e11092cd63",
"last_updated": "2025-03-30T03:08:55.422644Z",
"logging": {
"enabled": true
},
"ref": "ebadd8bb68d14db088bc40e11092cd63",
"version": "3"
},
{
"action": "execute",
"action_parameters": {
"id": "efb7b8c949ac4650a09736fc376e9aee",
"overrides": {
"action": "log"
},
"version": "latest"
},
"description": "ログモードのホスト",
"enabled": true,
"expression": "(http.host wildcard \"*.dev.oymk.work\")",
"id": "da192ff50acc4e7d8d48387f85634299",
"last_updated": "2025-03-30T03:06:02.71743Z",
"ref": "da192ff50acc4e7d8d48387f85634299",
"version": "2"
},
{
"action": "execute",
"action_parameters": {
"id": "efb7b8c949ac4650a09736fc376e9aee",
"overrides": {
"action": "managed_challenge"
},
"version": "latest"
},
"description": "チャレンジモードのホスト",
"enabled": true,
"expression": "(http.host wildcard \"*.prod.oymk.work\")",
"id": "95b4b9b267234ec9bedb9bf7bc27e809",
"last_updated": "2025-03-30T03:06:48.599211Z",
"ref": "95b4b9b267234ec9bedb9bf7bc27e809",
"version": "1"
}
],
"source": "firewall_managed",
"version": "96"
}
追えば詳細はわかりますが、直感的ではないですね。
JSON Crack
パット見でわかるように、出力を JSON Crack に通します。
エントリーポイントルールセットからの分岐(各ルールセットの呼び出し)がわかりやすくなりました。
ダッシュボードに近く
必要なものだけ取り出したり、キーを書き直したりするのも、いいかもしれません。
node --no-warnings rulesets.get.account.phase.entry.ts \
| jq '.|{k:.kind,n:.name,p:.phase,r:[.rules[]|{e:.enabled,a:.action,x:.expression,d:.description,v:.version}]}' \
| jq -s '.[]|select(.p=="http_request_firewall_managed")'
出力
{
"k": "root",
"n": "root",
"p": "http_request_firewall_managed",
"r": [
{
"e": false,
"a": "skip",
"x": "(http.request.uri.path wildcard \"/skip/*\")",
"d": "スキップ",
"v": "3"
},
{
"e": true,
"a": "execute",
"x": "(http.host wildcard \"*.dev.oymk.work\")",
"d": "ログモードのホスト",
"v": "2"
},
{
"e": true,
"a": "execute",
"x": "(http.host wildcard \"*.prod.oymk.work\")",
"d": "チャレンジモードのホスト",
"v": "1"
}
]
}
詳細を省いたので、見やすく、ダッシュボードに近くなりました。
ゾーンレベル
ゾーンレベルのエントリーポイントルールセットを全て取得します。
アカウント ID
の代わりにゾーン ID
を指定、kind
を root
から zone
に、が変更点になります。
rulesets.get.zone.phase.entry.ts
Cloudflare.rulesets.list
Cloudflare.rulesets.phases.get
const Cloudflare = require('cloudflare')
const dotenv = require('dotenv')
dotenv.config()
const cloudflare = new Cloudflare({
apiToken: process.env['CLOUDFLARE_TOKEN'],
})
async function main() {
const rulesetsList = await cloudflare.rulesets.list({
//account_id: process.env['ACCOUNT_ID'],
zone_id: process.env['ZONE_ID_O']
})
//const entryRulesets = rulesetsList.result.filter(ruleset => ruleset.kind === 'root')
const entryRulesets = rulesetsList.result.filter(ruleset => ruleset.kind === 'zone')
let entryPhases = entryRulesets.map(ruleset => ruleset.phase)
entryPhases = [...new Set(entryPhases)]
entryPhases.forEach(async entryPhase => {
const ruleset = await cloudflare.rulesets.phases.get(
entryPhase,
{
//account_id: process.env['ACCOUNT_ID'],
zone_id: process.env['ZONE_ID_O']
}).catch(async (err) => {
if (err instanceof Cloudflare.APIError) {
console.log(err.status)
} else {
throw err
}
})
console.log(JSON.stringify(ruleset))
})
}
main()
このゾーンで有効なフェーズは以下でした。
リクエスト・レスポンスに対して複数のフェーズでルールが適用されてます。
[
'http_request_firewall_custom',
'http_config_settings',
'http_request_firewall_managed',
'http_request_cache_settings',
'http_response_firewall_managed',
'http_ratelimit',
'http_request_origin',
'http_request_late_transform',
'http_response_headers_transform',
'http_request_dynamic_redirect',
'http_request_transform',
'ddos_l7'
]
このなかで http_request_firewall_managed
と http_request_firewall_custom
はアカウントレベルにも存在します。
これらのフェーズでは アカウントレベル → ゾーンレベル
の順にルールが適用されることになります。
ダッシュボード
ゾーン(ドメイン)のダッシュボードを見てみます。
アカウント > ドメイン > セキュリティ(旧) > WAF > 管理ルール
アカウント > ドメイン > セキュリティ(新) > セキュリティルール > 管理ルール
この画面が http_request_firewall_managed
のエントリーポイントルールセット と言えます。
管理ルールセット アカウントレベルとの違い
アカウントレベルのダッシュボードと比較するとわかりますが、アカウントレベルと違い、Cloudflare 管理ルールセット
を 1 つしかデプロイできません。
API
API で取得します。
node --no-warnings rulesets.get.zone.phase.entry.ts \
| jq -s '.[]|select(.phase=="http_request_firewall_managed")'
出力
API の出力にはダッシュボードより一階層深い情報も含まれています。
{
"description": "",
"id": "6b4477493204492fa88182247e2c543d",
"kind": "zone",
"last_updated": "2025-03-30T08:28:24.779072Z",
"name": "default",
"phase": "http_request_firewall_managed",
"rules": [
{
"action": "skip",
"action_parameters": {
"rules": {
"efb7b8c949ac4650a09736fc376e9aee": [
"c15c5a490f004b7fa7c79c73b1df0e15"
]
}
},
"description": "skip this rule",
"enabled": true,
"expression": "(starts_with(http.request.uri.path, \"/thispath/\"))",
"id": "4bd8ac6a5c064e368a82a3c2aa81f8c6",
"last_updated": "2025-03-30T08:28:24.779072Z",
"logging": {
"enabled": true
},
"ref": "4bd8ac6a5c064e368a82a3c2aa81f8c6",
"version": "2"
},
{
"action": "execute",
"action_parameters": {
"id": "efb7b8c949ac4650a09736fc376e9aee",
"matched_data": {
"public_key": "p"
},
"overrides": {
"rules": [
{
"enabled": true,
"id": "882b37d6bd5f4bf2a3cdb374d503ded0"
},
{
"enabled": true,
"id": "6e759e70dc814d90a003f10424644cfb"
},
{
"action": "log",
"enabled": true,
"id": "ee922cf00077462d9f2f7330b114b839"
}
]
},
"version": "latest"
},
"enabled": true,
"expression": "true",
"id": "a764e5988cc3446983ae3b635988c4c2",
"last_updated": "2024-09-13T01:58:01.808585Z",
"ref": "a764e5988cc3446983ae3b635988c4c2",
"version": "15"
},
{
"action": "execute",
"action_parameters": {
"id": "4814384a9e5d4991b9815dcfc25d2f1f",
"matched_data": {
"public_key": "p"
},
"overrides": {
"categories": [
{
"category": "paranoia-level-2",
"enabled": false
},
{
"category": "paranoia-level-3",
"enabled": false
},
{
"category": "paranoia-level-4",
"enabled": false
}
],
"rules": [
{
"action": "managed_challenge",
"id": "6179ae15870a4bb7b2d480d4843b323c",
"score_threshold": 40
}
]
},
"version": "latest"
},
"enabled": true,
"expression": "true",
"id": "addef11c3c0b4af78bcd89f90b1087b3",
"last_updated": "2024-06-03T11:55:56.735012Z",
"ref": "addef11c3c0b4af78bcd89f90b1087b3",
"version": "5"
},
{
"action": "execute",
"action_parameters": {
"id": "c2e184081120413c86c3ab7e14069605",
"matched_data": {
"public_key": "p"
},
"overrides": {
"enabled": true
},
"version": "latest"
},
"enabled": false,
"expression": "true",
"id": "638c6dc77167490db28250aebdcd8ea8",
"last_updated": "2025-03-14T07:17:00.001498Z",
"ref": "638c6dc77167490db28250aebdcd8ea8",
"version": "9"
}
],
"source": "firewall_managed",
"version": "30"
}
JSON Crack
出力を JSON Crack に食わせます。
見通しは良くなりました。
ダッシュボードに近く
出力を簡素にしてみます。
node --no-warnings rulesets.get.zone.phase.entry.ts \
| jq '.|{k:.kind,n:.name,p:.phase,r:[.rules[]|{e:.enabled,a:.action,x:.expression,d:.description,v:.version}]}' \
| jq -s '.[]|select(.p=="http_request_firewall_managed")'
出力
{
"k": "zone",
"n": "default",
"p": "http_request_firewall_managed",
"r": [
{
"e": true,
"a": "skip",
"x": "(starts_with(http.request.uri.path, \"/thispath/\"))",
"d": "skip this rule",
"v": "2"
},
{
"e": true,
"a": "execute",
"x": "true",
"d": null,
"v": "15"
},
{
"e": true,
"a": "execute",
"x": "true",
"d": null,
"v": "5"
},
{
"e": false,
"a": "execute",
"x": "true",
"d": null,
"v": "9"
}
]
}
ルールの取得(とオーバーライド)
個々のルールの詳細を確認する例です。
ダッシュボード
たとえば WAF 管理ルールセット
の設定を見ると、デフォルトのルールからオーバーライド
されている箇所があります。オーバーライドはデフォルトの WAF 管理ルールを変更したいときに利用します。
下図の一番上は呼び出されたデフォルトのルールのうち、 ID 882b37d6bd5f4bf2a3cdb374d503ded0
がエントリーポイントルールセットにより上書きされていることを示します。
アカウント > ドメイン > セキュリティ(旧) > WAF > 管理ルール > 管理ルール例外を編集する
アカウント > ドメイン > セキュリティ(新) > セキュリティルール > 管理ルール > 管理ルール例外を編集する
デフォルトのルールセットをエントリーフェーズルールセットから呼び出し、特定ルールを上書き
API
http_request_firewall_managed
のエントリーポイントルールセットで overrides
に当たります。
ダッシュボードに表示されているデフォルトルールを知りたい場合は、この id
を使ってルールを呼び出します。
ルールセット中のオーバーライド
{
"action": "execute",
"action_parameters": {
"id": "efb7b8c949ac4650a09736fc376e9aee",
"matched_data": {
"public_key": "p"
},
"overrides": {
"rules": [
{
"enabled": true, <<=====
"id": "882b37d6bd5f4bf2a3cdb374d503ded0" <<=====
},
{
"enabled": true,
"id": "6e759e70dc814d90a003f10424644cfb"
},
{
"action": "log",
"enabled": true,
"id": "ee922cf00077462d9f2f7330b114b839"
}
]
},
"version": "latest"
},
:
ルールセットに定義されるルールを取るサンプルコード(ゾーンレベル)は以下です。
rulesets.get.zone.rules.ts
Cloudflare.rulesets.list
Cloudflare.rulesets.get
const Cloudflare = require('cloudflare')
const dotenv = require('dotenv')
dotenv.config()
const cloudflare = new Cloudflare({
apiToken: process.env['CLOUDFLARE_TOKEN'],
})
async function main() {
const rulesetsList = await cloudflare.rulesets.list({
zone_id: process.env['ZONE_ID_O']
})
const allRulesets = rulesetsList.result
let allRules = allRulesets.map(ruleset => ruleset.id)
allRules = [...new Set(allRules)]
allRules.forEach(async ruleId => {
const rule = await cloudflare.rulesets.get(
ruleId,
{
zone_id: process.env['ZONE_ID_O']
}).catch(async (err) => {
if (err instanceof Cloudflare.APIError) {
console.log(err.status)
} else {
throw err
}
})
console.log(JSON.stringify(rule))
})
}
main()
ルールの id
で出力をフィルタします。
node --no-warnings rulesets.get.zone.rules.ts \
|jq '.|select(.phase=="http_request_firewall_managed")|.rules[]|select(.id=="882b37d6bd5f4bf2a3cdb374d503ded0")'
この WAF 管理ルールのデフォルトの enable
は false
なことがわかります。
つまりルールとしては存在するものの、デフォルトでは有効ではありません。
{
"action": "block",
"categories": [
"html-injection",
"xss"
],
"description": "XSS, HTML Injection",
"enabled": false, <<=====
"id": "882b37d6bd5f4bf2a3cdb374d503ded0", <<=====
"last_updated": "2025-03-28T19:58:07.539956Z",
"ref": "faf50d2a08a76c8531c30147129cd6bf",
"version": "156"
}
エントリーポイントルールセットから呼び出すときに、この enabled
を true
に上書きしていることになります。
"overrides": {
"rules": [
{
"enabled": true, <<=====
"id": "882b37d6bd5f4bf2a3cdb374d503ded0" <<=====
},
最近のダッシュボード
ダッシュボードは生き物で、変わります。
ルールセット利用のプロダクト機能(フェーズ)については、API でエントリーポイントルールセットを一覧したときのような UI になっています。
ゾーンレベルだと下記のとおりです。執筆時点(2025/04)
最後に
Ruleset Engine は様々なプロダクトで活用されています。
API での運用ツールも複数用意されています。
Cloudflare 全体での運用管理の効率化に活かしていただければと思います。
Discussion