🐈

Cloudflare Ruleset を API で把握する(cloudflare-typescript と JSON Crack)

に公開

はじめに

Cloudflare のプロダクトを見ると Ruleset Engine を利用するものが増殖しています。
この記事ではルールセットを API で把握する例を示し、運用に活かすことを考えます。


Google 翻訳

基本用語

  • Phases
    • リクエストやレスポンスに対する複数の処理フェーズ( WAF や Rate Limit など)
    • 各フェーズにルールセットを定義(アカウントレベルあるいはゾーンレベル)
      • アカウントレベルはエンタープライズプランのみ
      • アカウントレベルがある場合、先に評価
  • Rulesets
    • それぞれのフェーズで実行するルールの塊
  • Rules
    • リクエストやレスポンスを評価する個々のルール


リクエスト・レスポンスはフェーズごとのルールセット内のルールで評価される

ルールセットの使われ領域

ダッシュボードではルールセットを利用しているプロダクト機能(フェーズ)確認することができます。
この記事ではそれぞれのルールセットの箱の中を API 確認する方法を書きます。

API での確認に使うツール

今回の確認には下記を利用しました。

https://github.com/cloudflare/cloudflare-typescript

  • サンプルコード
    API Dev docsTypeScript を選べば cloudflare-typescript のサンプルを確認できます。

  • 可視化
    API を叩いた結果の JSON を見やすくするために、JSON Crack を使います。
    VS Code の extension があったので、使いました。

https://github.com/AykutSarac/jsoncrack.com

実践

まずエントリーポイントから始めよ

ルールセットの確認でまず重要なのはフェーズのエントリーポイントルールセットです。

下記の役割があります。

  • そのフェーズにリクエストやレスポンスを引き込む
  • マッチ評価を行い、アクション
    • ルールの適用(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()

このアカウントでは以下の複数のフェーズが有効になっていました。

console.log(entryPhases)
[
  '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 を指定、kindroot から 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()

このゾーンで有効なフェーズは以下でした。
リクエスト・レスポンスに対して複数のフェーズでルールが適用されてます。

console.log(entryPhases)
[
  '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_managedhttp_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 管理ルールのデフォルトの enablefalse なことがわかります。
つまりルールとしては存在するものの、デフォルトでは有効ではありません。

デフォルトルール
{
  "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"
}

エントリーポイントルールセットから呼び出すときに、この enabledtrue に上書きしていることになります。

オーバーライド
        "overrides": {
          "rules": [
            {
              "enabled": true,                          <<=====
              "id": "882b37d6bd5f4bf2a3cdb374d503ded0"  <<=====
            },

最近のダッシュボード

ダッシュボードは生き物で、変わります。

ルールセット利用のプロダクト機能(フェーズ)については、API でエントリーポイントルールセットを一覧したときのような UI になっています。

ゾーンレベルだと下記のとおりです。執筆時点(2025/04)

最後に

Ruleset Engine は様々なプロダクトで活用されています。
API での運用ツールも複数用意されています。

Cloudflare 全体での運用管理の効率化に活かしていただければと思います。

Discussion