Cloudflare で Credential Stuffing 対策（Exposed Credentials Check 編）

## Update があります 2025.3
https://zenn.dev/oymk/articles/0c6e88138dc909

## はじめに
Credential Stuffing 対策の一つとして Cloudflare WAF 機能の一つである [Exposed Credentials Check](https://developers.cloudflare.com/waf/exposed-credentials-check/) を紹介します。

## Credential Stuffing について

[Credential Stuffing](https://www.cloudflare.com/ja-jp/learning/bots/what-is-credential-stuffing/) は、あるサービスで漏洩した認証資格情報を使い、他のサービスでログイン試行することを指します。認証資格情報は使い回されがちなので、そこが狙われます。
<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/b8751a17-f2b6-a945-07e0-360fa8f9a9fe.png">

自分のユーザー名やパスワードがどこかに漏れてるんじゃないかと "[Have I Been Pwned](https://haveibeenpwned.com/)” などで確認される方もいらっしゃるのではないでしょうか。

そのようなサイトでは、これまでに漏洩した認証資格情報がデータベース化されており、ユーザーは自分（あるいは他の誰か）に関連したユーザー名やパスワードがそこに含まれるかを確認することができます。
<img width="400" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/cf8cea05-6fc0-0b8a-6ded-e8c4c832c9c1.png">
<img width="400" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/80007795-349f-c2fc-6875-04df97446903.png">

HIBP には最近の情報漏洩と思われるデータも既に含まれていそうです。
「[光回線サービスの東名に不正アクセス、顧客情報の一部流出の可能性](https://scan.netsecurity.ne.jp/article/2023/10/30/50166.html)」
<img width="400" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/2a42d8b0-1e2a-86d6-8e44-02b2fe89fa20.png">
<img width="400" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/b817603c-0642-ec92-a133-b6d45d73bcbc.png">

なお、Cloudflare もデモサイト [MIGP "Might I Get Pwned"](https://migp.cloudflare.com/) を公開しています。
[背景 Blog](https://blog.cloudflare.com/privacy-preserving-compromised-credential-checking/)

## Cloudflare Proxy での対策と Exposed Credentials Check

Cloudflare Proxy を Eyeball と Web アプリケーションの間にオンパス（on-path）で入れることで、この種の[攻撃緩和に使う](https://blog.cloudflare.com/account-takeover-protection/)ことができます。

その中の一つが Exposed Credentials Check です。

|プロダクトの例|内容|
|-|-|
|[Open Proxy managed List](https://developers.cloudflare.com/waf/tools/lists/managed-lists/)|攻撃の踏み台に利用されている Open Proxy のリストを参照し、そこからのリクエストを制御|
|[Bot Management](https://developers.cloudflare.com/bots/plans/bm-subscription/) / [Super Bot Fight Mode](https://developers.cloudflare.com/bots/plans/biz-and-ent/)|ボットスコアリングにより、ボットらしいリクエストを制御|
|[Exposed Credential Checks](https://developers.cloudflare.com/waf/exposed-credentials-check/)|公に漏洩しているユーザー名/パスワードの情報とリクエストの入力値を照合|
|[Rate Limiting](https://developers.cloudflare.com/waf/rate-limiting-rules/) on failed logins|ログイン失敗のイベントを観測し、しきい値以上のリクエストを制御<br>*ログイン失敗時にオリジンサーバーが任意のレスポンスヘッダーやステータスコード（4xxなど）で状況を伝えてくれれば[追随できる](https://qiita.com/KeioCF/items/31b7014c940d5aba741e#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%82%92%E6%9D%A1%E4%BB%B6%E3%81%AB)|
|[Cloudflare Access Managed Devices](https://blog.cloudflare.com/zero-trust-with-managed-devices/)|自組織で管理されたデバイスからのリクエストのみを許可|

```mermaid
flowchart LR
    Z[Eyeball] -->|Open Proxies?| B
    Y[Eyeball] -->|Managed Devices?| B
    X[Eyeball] -->|Leaked Passwords?| B
    W[Eyeball] -->|Excessive login failures?| B
    V[Eyeball] -->|Bot networks?| B((Cloudflare<br>Credentials Stuffing対策))
    B --> |mitigate<br>notify| C[Origin Server<br>!ログイン!]
```
```mermaid
flowchart LR
    Z[Eyeball] -->|Open Proxies| C
    Y[Eyeball] -->|Unmanaged Devices| C
    X[Eyeball] -->|Leaked Passwords| C
    W[Eyeball] -->|Excessive login failures| C
    V[Eyeball] -->|Bot networks| C[Origin Server<br>!ログイン!]
```

## 前提：Exposed Credentials Check とプライバシー保護

ユーザー名とパスワードは超機密情報です。

Exposed Credentials Check では、プライバシー保護に対するアプローチとして、これらの情報が WAF 以外のプロセスにさらされない、また、データベースとの照合はハッシュをもってやり取りされる、といった対策が[実装](https://blog.cloudflare.com/privacy-preserving-compromised-credential-checking/)されています。

## どのようなアプリケーションのログインページに対応しているか

[本家](https://developers.cloudflare.com/waf/managed-rules/reference/exposed-credentials-check/)にリストがありますが、API でも Description と Categories がとれますので、それを記します。
"各アプリケーションのログインページ"および"汎用的なログインページ"への対応が可能です。

|Description|Categories|Action<br>(default)|
|-|-|-|
|Blocks requests containing 'Exposed-Credential-Check' headers, as these can be used to trick the origin into believing a request contained (or didn't contain) an exposed credential|header|block|
|Checks credentials from the default drupal login page |drupal|rewrite|
|Checks credentials in POST forms using "username" and "password" arguments|application-multi|rewrite|
|Checks credentials in POST forms using "login" and "password" arguments (URI agnostic)|application-multi|rewrite|
|Checks credentials from the default ghost login page|ghost|rewrite|
|Checks credentials from the default joomla login page|joomla|rewrite|
|Checks credentials from the default magento login page|magento|rewrite|
|Checks credentials from the default plone login page|plone|rewrite|
|Checks credentials from the default wordpress login page|wordpress|rewrite|
|Checks credentials sent as JSON with 'email' and 'password' keys|application-multi|rewrite|
|Checks credentials sent as JSON with 'username' and 'password' keys|application-multi|rewrite|
|Beta rule 3|application-multi|rewrite|
|Beta rule 4|application-multi|rewrite|
|Checks Microsoft Exchange OWA credentials|microsoft-exchange-owa|rewrite|

四番目の form login の Description に URI agnostic とあるのは 三番目の form username のほうに対象の URI が暗に定義されているためです。
form login や JSON は URI に関わらずマッチします。


<details><summary>取得例</summary>

```shell
$ curl -s -X GET -H "Content-Type: application/json" -H "X-Auth-Email: $EMAIL" -H  "X-Auth-Key: $API_KEY" "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$RULESET"|jq -r '.result.rules[]|[.description, .categories[], .action]|@tsv'
Blocks requests containing 'Exposed-Credential-Check' headers, as these can be used to trick the origin into believing a request contained (or didn't contain) an exposed credential	header	block
Checks credentials from the default drupal login page 	drupal	rewrite
Checks credentials in POST forms using "username" and "password" arguments	application-multi	rewrite
Checks credentials in POST forms using "login" and "password" arguments (URI agnostic)	application-multi	rewrite
Checks credentials from the default ghost login page	ghost	rewrite
Checks credentials from the default joomla login page	joomla	rewrite
Checks credentials from the default magento login page	magento	rewrite
Checks credentials from the default plone login page	plone	rewrite
Checks credentials from the default wordpress login page	wordpress	rewrite
Checks credentials sent as JSON with 'email' and 'password' keys	application-multi	rewrite
Checks credentials sent as JSON with 'username' and 'password' keys	application-multi	rewrite
Beta rule 3	application-multi	rewrite
Beta rule 4	application-multi	rewrite
Checks Microsoft Exchange OWA credentials	microsoft-exchange-owa	rewrite
```

</details>

## ユーザー名とパスワードの入力フィールド名

各アプリケーション向けのルールについては、ログインで利用されるユーザー名とパスワードのフィールド名がそれぞれ定義されています。

その他、汎用的なログイン情報入力フィールドとして、下記のルールが[定義されています](https://developers.cloudflare.com/waf/managed-rules/reference/exposed-credentials-check/)。

> - Check forms submitted using a POST request containing username and password arguments
> - Check credentials sent as JSON with email and password keys
> - Check credentials sent as JSON with username and password keys

|メソッド|入力形式|content-type|ユーザー名のフィールド名|パスワードのフィールド名|
|-|-|-|-|-|
|POST|フォーム|application/x-www-form-urlencoded|username|password|
|POST|フォーム|application/x-www-form-urlencoded|login|password|
|POST|JSON|application/json|email|password|
|POST|JSON|application/json|username|password|

フォームの login については devdoc に記載がないですが、API のレスポンスには明記されているので入れてます。

## Action（検知された場合の挙動）

検知された場合の [Action](https://developers.cloudflare.com/firewall/cf-firewall-rules/actions/) については、[Exposed-Credential-Check Header](https://developers.cloudflare.com/waf/exposed-credentials-check/#available-actions), [Managed Challenge](https://developers.cloudflare.com/firewall/cf-firewall-rules/cloudflare-challenges/#managed-challenge-recommended), Block, [JS Challenge](https://developers.cloudflare.com/firewall/cf-firewall-rules/cloudflare-challenges/#js-challenge), Log, and [Interactive Challenge](https://developers.cloudflare.com/firewall/cf-firewall-rules/cloudflare-challenges/#interactive-challenge) が[使えます](https://developers.cloudflare.com/waf/managed-rules/reference/exposed-credentials-check/#configure-in-the-dashboard)。

漏洩情報が使用されたことをオリジンサーバに示すため、リクエストヘッダーに Exposed-Credential-Check を追加するのがデフォルトです。
オリジンサーバー側で何かしらの対応ができるようになります。

```mermaid
sequenceDiagram
    Eyeball->>+Cloudflare: POST Body<br>username:<leaked>, password:<leaked>
    Cloudflare->>+Cloudflare: Exposed Credentials Check (match!)
    Cloudflare->>+Origin App: HTTP Header<br>Exposed-Credential-Check: 1
    Origin App->>+Origin App: do something
```

## 設定

Zone レベルで設定をします。
Zone の WAF > Managed rules から "Cloudflare Leaked Credentials Check" を Enabled にします。
すべてのリクエストがこの Ruleset で評価され始めます。

<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/ae8d1027-8ed5-25ed-31bd-f00c085e9af8.png">

設定をデフォルトから変更する場合は青字の名前部分をクリック（または右のミートボールから Edit を選択）し、[変更](https://developers.cloudflare.com/waf/managed-rules/reference/exposed-credentials-check/#configure-in-the-dashboard)画面に遷移します。
<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/6e7e4ecb-6609-d484-5e20-49bb78458819.png">

他の Managed Ruleset と同じ手順ですね。
今回はデフォルトで行きます。

## 使用例
テスト用のダミーの[ユーザー名とパスワード](https://developers.cloudflare.com/waf/exposed-credentials-check/test-configuration/)がありますので、そちらを試します。

|key|value|
|-|-|
| username | CF_EXPOSED_USERNAME<span>@</span>example.com |
| password | CF_EXPOSED_PASSWORD |

username1<span>@</span>example.com
password1
も[使えました](https://blog.cloudflare.com/privacy-preserving-compromised-credential-checking/)。

### Eyeball
```shell
curl -s 'https://www2.oyama.cf/login' -X POST  -H 'Content-Type: application/x-www-form-urlencoded' -d 'login=CF_EXPOSED_USERNAME%40example.com&password=CF_EXPOSED_PASSWORD'
```

### オリジンサーバー
オリジンサーバーが受信したリクエストヘッダーには Exposed-Credential-Check が 1 でセットされています。
```
  Exposed-Credential-Check: 1
```
マッチしない場合はヘッダー自体がセットされません。

## 観測
Security Events ダッシュボードで簡単に[イベント発生状況を得る](https://developers.cloudflare.com/waf/exposed-credentials-check/monitor-events/)ことができます。
どれくらい Leaked Password を使ったログインが施行されたか、わかりますね。

<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/52af7d39-91fe-ada4-c9d0-0232bfb498be.png">

個別のイベントをみると、一つ一つのイベントの詳細がわかります。
- Service: Managed rules
- Ruleset: Cloudflare Exposed Credentials Check Ruleset
- Rule: Checks credentials in POST forms using "username" and "password" arguments
- Action taken: Rewrite

<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/814bc1c4-6f33-9c3a-9206-a75624e2f3d2.png">

## 通知

WAF の一部なので、イベントのスパイク時には Notification の機能で[通知する](https://qiita.com/KeioCF/items/c4df3a122c74b29aa902)ことができます。
<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/a81d8915-871f-e381-f710-c027485f531f.png">

この状況では 260 を越えたあたりで Slack に通知が来ました。
<img width="400" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/5ee4d16a-6b7a-be17-c04d-7c63d42fcd17.png">

## ログ

[HTTP Requests](https://developers.cloudflare.com/logs/reference/log-fields/zone/http_requests/) あるいは [Firewall Events](https://developers.cloudflare.com/logs/reference/log-fields/zone/firewall_events/) で取ることができます。

Firewall Events で [Payload logging](https://developers.cloudflare.com/waf/managed-rules/payload-logging/configure/) を有効にしている場合、Event ログに暗号化された Payload データが追加されます。
フォームの場合、そのデータを復号化すると、下記のようにユーザ名/パスワードが確認できます。JSON ではユーザー名/パスワードは[含まれません](https://developers.cloudflare.com/waf/exposed-credentials-check/monitor-events/)。

```JSON:payload 例（フォーム）
{
  "http.request.method": "POST",
  "http.request.headers": {
    "accept": [
      "*/*"
    ],
    "content-length": [
      "68"
    ],
    "content-type": [
      "application/x-www-form-urlencoded"
    ],
    "host": [
      "www2.oyama.cf"
    ],
    "user-agent": [
      "curl/8.2.1"
    ],
    "x-forwarded-proto": [
      "https"
    ]
  },
  "http.request.body.form": {
    "login": [
      "CF_EXPOSED_USERNAME%40example.com"
    ],
    "password": [
      "CF_EXPOSED_PASSWORD"
    ]
  }
}
```
ユーザ名とパスワードが表示されるのでドキッとしますが、 Payload logging はそもそも[プライバシー保護](https://blog.cloudflare.com/encrypt-waf-payloads-hpke/)のための技術なので、ここで見えているユーザ名とパスワードは秘密鍵を持つユーザー管理者のみが閲覧できます。

## 中締め
以上 Zone レベルでの Exposed Credentials Check の設定および動作概要でした。
以降は Zone レベルの挙動では足りない場合の Account レベルでの設定を紹介します。

*[Account level WAF](https://blog.cloudflare.com/account-waf/) が必要になります。

## Account レベル WAF（Managed / Custom Rulesets）での Exposed Credeintials Check 対応

Zone レベルでの Exposed Credentials Check で足りない[場合](https://developers.cloudflare.com/waf/exposed-credentials-check/#exposed-credentials-checks-in-custom-rules)があります。
たとえば下記のようなケースです。
これらに対しては Account level WAF の Managed / Custom Rulesets で対応します。

|番号|ユースケース|対応する Ruleset<br>（Account level）|Phase|
|-|-|-|-|
|1|Exposed Credentials Check の Managed ruleset に対象アプリケーションがない、または、ユーザー名/パスワードの入力フィールドをデフォルトから変えたい。<br>また、特定のホストやパスを対象にしたい。|Custom ruleset|http_request_firewall_custom|
|2|Exposed Credentials Check の Rule は Managed ruleset で足りるが、特定の Zone 、ホスト、パスだけ Exposed Credentials Check の対象にしたい。|Managed ruleset|http_request_firewall_managed|
|3|Exposed Credentials Check の Rule は Managed ruleset で足りるが、Account 全体に共通の Exposed Credentials Check を仕込みたい。|Managed ruleset|http_request_firewall_managed|


Account レベル WAF の設定画面を見ると、[Custom rulesets](https://developers.cloudflare.com/waf/custom-rulesets/) と [Managed rulesets](https://developers.cloudflare.com/waf/managed-rules/deploy-account-dashboard/) それぞれの設定タブが用意されています。
<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/d4628ac8-32a0-a686-383c-6143e866b119.png">

ただ、上記のケース 1 の Exposed Credentials Check の Custome Rules についてはダッシュボードでの設定が現時点では[サポートされていない](https://developers.cloudflare.com/waf/exposed-credentials-check/#exposed-credentials-checks-in-custom-rules)ので [API](https://developers.cloudflare.com/waf/exposed-credentials-check/configure-api/) から定義します。

今回は 1 のケースに実際に対応してみます。

### Exposed Credentials Check 用 Custom Ruleset 設定例

対象のアプリケーションは下記の仕様だとします。

|入力形式|ユーザー名のフィールド名<br>（username_expression）|パスワードのフィールド名<br>（password_expression）|パス<br>（expression）|
|-|-|-|-|
|JSON|onamae|passwd|/my-login|


Custom Ruleset を使い、これに対応する Exposed Credentials Check を仕込むことになります。


### Custom Ruleset データ（JSON）の準備

リクエエストのマッチ条件と、マッチした際のアクションを定義します。

下記のような内容で Ruleset 設定用の [JSON データ](https://developers.cloudflare.com/waf/exposed-credentials-check/configure-api/#example-b)を作ります。
この場合、Ruleset に含まれる Rule は一つですが、複数入れることもできます。

|Ruleset|value|
|-|-|
|phase|http_request_firewall_custom|
|name|Custom Ruleset A|
|description|This ruleset includes a rule checking for exposed credentials.|
|kind|custom|
|rules|個々の Rule が入る<br>⇓|

|Rule|value|備考|
|-|-|-|
|expression|http.request.method == \"POST\" && http.request.uri == \"/my-login\" && any(http.request.headers[\"content-type\"][*] == \"application/json\")|リクエストをこのルールにマッチさせるための表現。<br>ここでは method,uri,content-typeを定義|
|action|rewrite||
|action_parameters|header Exposed-Credential-Check に 1 をset|rewrite の具体的な内容|
|exposed_credential_check|"username_expression": lookup_json_string(http.request.body.raw, \"onamae\") <br>"password_expression": lookup_json_string(http.request.body.raw, \"passwd\")|リクエストからユーザ名とパスワードを入手するための表現。<br>onamae と passwd というフィールド名（JSON Key）を探す|
|description|Exposed credential check on my-login endpoint with JSON body||

```json:rulesetA: Ruleset 設定用データ
{
  "name": "Custom Ruleset A",
  "kind": "custom",
  "description": "This ruleset includes a rule checking for exposed credentials.",
  "rules": [
    {
      "action": "rewrite",
      "action_parameters": {
        "headers": {
          "Exposed-Credential-Check": {
            "operation": "set",
            "value": "1"
          }
        }
      },
      "description": "Exposed credential check on my-login endpoint with JSON body",
      "expression": "http.request.method == \"POST\" && http.request.uri == \"/my-login\" && any(http.request.headers[\"content-type\"][*] == \"application/json\")",
      "exposed_credential_check": {
        "username_expression": "lookup_json_string(http.request.body.raw, \"onamae\")",
        "password_expression": "lookup_json_string(http.request.body.raw, \"passwd\")"
      }
    }
  ],
  "phase": "http_request_firewall_custom"
}
```

### Custom Ruleset の作成
このデータを Account の ruleset エンドポイントに対して POST することで、ruleset を作成することができます。
→ [create ruleset](https://developers.cloudflare.com/ruleset-engine/rulesets-api/create/)

```shell: 作成用 API
 $ curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/rulesets" \
--header 'X-Auth-Email: '$EMAIL'' --header 'X-Auth-Key: '$API_KEY'' --header 'Content-Type: application/json' --data '@rulesetA' -s| jq '.'
```
作成がうまく行き、応答で下記の追加情報を得ました。

- Ruleset

|Ruleset|value|備考|
|-|-|-|
|id|ef8cfa59128e4d669d7b90056ca8dab7|今後 Ruleset を 操作（Update など）するときに使う|
|source|firewall_custom||
|version|1|Update するたびに増える|
|last-updated|2023-11-04T08:28:25.787328Z|Rulesetの最終更新日時|

- Rule

|Rule|value|備考|
|-|-|-|
|id|f75fcd3230fe42ae8a5c144404283e18|今後 Rule を 操作（Update など）するときに使う|
|version|1|Update するたびに増える|
|enabled|true|POST 時に指定しなければデフォルト true|
|last-updated|2023-11-04T08:28:25.787328Z|Ruleの最終更新日時|

Ruleset, Rule それぞれが ID とバージョンで識別されていることがわかります。

```json:応答
{
  "result": {
    "id": "ef8cfa59128e4d669d7b90056ca8dab7",
    "name": "Custom Ruleset A",
    "description": "This ruleset includes a rule checking for exposed credentials.",
    "source": "firewall_custom",
    "kind": "custom",
    "version": "1",
    "rules": [
      {
        "id": "f75fcd3230fe42ae8a5c144404283e18",
        "version": "1",
        "action": "rewrite",
        "exposed_credential_check": {
          "username_expression": "lookup_json_string(http.request.body.raw, \"onamae\")",
          "password_expression": "lookup_json_string(http.request.body.raw, \"passwd\")"
        },
        "expression": "http.request.method == \"POST\" && http.request.uri == \"/my-login\" && any(http.request.headers[\"content-type\"][*] == \"application/json\")",
        "description": "Exposed credential check on my-login endpoint with JSON body",
        "last_updated": "2023-11-04T08:28:25.787328Z",
        "ref": "f75fcd3230fe42ae8a5c144404283e18",
        "enabled": true,
        "action_parameters": {
          "headers": {
            "Exposed-Credential-Check": {
              "operation": "set",
              "value": "1"
            }
          }
        }
      }
    ],
    "last_updated": "2023-11-04T08:28:25.787328Z",
    "phase": "http_request_firewall_custom"
  },
  "success": true,
  "errors": [],
  "messages": []
}
```

#### 補足：Ruleset アップデート

設定済のデータをアップデートしたい場合は新しいデータを PUT します。 

→ [update ruleset](https://developers.cloudflare.com/ruleset-engine/rulesets-api/update/)

expression を下記のように変更してアップデートしてみます。
*パス /my-login を削除（追って別の場所で指定するため冗長回避）

|Before|After|
|-|-|
|http.request.method == \"POST\" && http.request.uri == \"/my-login\" && any(http.request.headers[\"content-type\"][*] == \"application/json\")|http.request.method eq \"POST\" and any(http.request.headers[\"content-type\"][*] eq \"application/json\")|


```shell: 作成用 API
 $ curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/rulesets/$RULESET_ID" \
--header 'X-Auth-Email: '$EMAIL'' --header 'X-Auth-Key: '$API_KEY'' --header 'Content-Type: application/json' -data '@rulesetA' -X PUT| jq '.'
```

戻りデータではバージョンが上がっています。

```json:戻りデータ
    "version": "2",
    "rules": [
      {
       :
       "expression": "http.request.method eq \"POST\" and any(http.request.headers[\"content-type\"][*] eq \"application/json\")",
       :
```
        

### Custom Ruleset のデプロイ

この時点ではまだ Ruleset はリクエストに反映されません。
作成された Ruleset のデプロイが必要になります。

API でデプロイするには [Entry point Ruleset](https://developers.cloudflare.com/ruleset-engine/about/rulesets/#entry-point-ruleset) を action: execute で[作成](https://developers.cloudflare.com/ruleset-engine/basic-operations/deploy-rulesets/)する必要があります。

Entry point ruleset（action: execute）について下の図で[説明](https://developers.cloudflare.com/ruleset-engine/about/rules/)します。

<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/d56dc81f-c1ea-7299-eea3-844dddf02b5c.png">

- オレンジ枠、黒枠、どちらも Ruleset を表します（その中に Rule が複数あります "R1,R2,.."）。
- 先に作成した Exposed Credentials Check 用の Custom Ruleset は黒枠に位置します。
- オレンジ枠が Entry point ruleset で、これから作ります。
- オレンジ枠の Ruleset を作り、その Rule の一つを黒枠の Ruleset に紐つけることで、黒枠 Ruleset が有効になります。
- リクエストがオレンジ枠に定義された Rule の expression にリクエストをマッチすると、紐付けられた [Managed Rulesets](https://developers.cloudflare.com/ruleset-engine/about/rulesets/#managed-rulesets) または [Custom Rulesets](https://developers.cloudflare.com/ruleset-engine/about/rulesets/#custom-rulesets) が Execute されます。（Entry point ruleset からみると Manage Ruleset や Custom Rulset は Rule 要素になります。）
- リクエストは Expression にマッチする限りそれぞれの[処理を受けます](https://developers.cloudflare.com/ruleset-engine/about/rules/)。
>"For a given incoming request, the expression of the first two rules matches the request properties. Therefore, the action for these rules runs (Execute and Log, respectively). "
- 同一 Phase 内で Field value は immutable で、Phase 内で変更されていても後続の Rule では[参照されません](https://developers.cloudflare.com/ruleset-engine/about/rules/#rule-evaluation-and-field-values)。別の Phase に渡ると変っていることがあります。
>”While evaluating rules for a given request/response, the values of all request and response fields are immutable within each phase. However, field values may change between phases."


#### Entry point ruleset 現状確認（デプロイ前）

http_request_firewall_custom フェーズの Account レベルで Entry point ruleset（オレンジ枠相当）を取ってみます。
entry point ruleset が見つからないと言われます。Ruleset が Deploy されていない状態です。

|フェーズ|レベル|entry point ruleset|
|-|-|-|
|http_request_firewall_custom|account|Deployされてない|


```shell
$ curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/rulesets/phases/http_request_firewall_custom/entrypoint" \
--header 'X-Auth-Email: '$EMAIL'' --header 'X-Auth-Key: '$API_KEY'' --header 'Content-Type: application/json' -s
{
  "result": null,
  "success": false,
  "errors": [
    {
      "message": "could not find entrypoint ruleset in the http_request_firewall_custom phase"
    }
  ],
  "messages": null
}
```

これを Deploy します。

#### Entry point ruleset データ（JSON）の準備
オレンジ枠にあたる Entry point ruleset として下記のようなデータを準備します。

```json:デプロイ用の Body
{
  "rules": [
    {
      "action": "execute",
      "action_parameters": {
        "id": "ef8cfa59128e4d669d7b90056ca8dab7"
      },
      "expression": "(http.host eq \"www2.oyama.cf\" and http.request.uri.path eq \"/my-login\")",
      "description": "Routing condition for an accont level exposed credentials check"
    }
  ]
}
```

|Rule|value|備考|
|-|-|-|
|action|execute|Entry point ruleset から Ruleset を呼び出し|
|action_parameters|ef8cfa59128e4d669d7b90056ca8dab7|呼び出す Ruleset（"Custom Ruleset A"）の ID|
|expression|(http.host eq \"www2.oyama.cf\" and http.request.uri.path eq \"/my-login\")|リクエストをこのルールにマッチさせるための表現<br># host と URI.path を指定|

*呼び出したい Ruleset ID は API で確認できます。
```shell: Ruleset ID を Ruleset の名前から探す
$ curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/rulesets" \
--header 'X-Auth-Email: '$EMAIL'' --header 'X-Auth-Key: '$API_KEY'' --header 'Content-Type: application/json' -s | jq -r '.result[]|select(.name=="Custom Ruleset A")|.id'
ef8cfa59128e4d669d7b90056ca8dab7
```

#### Entry point ruleset の作成
Entry point ruleset（オレンジ枠相当）は [PUT で配置](https://developers.cloudflare.com/ruleset-engine/rulesets-api/update/)します。
無事に作成されれば、ホスト名とパスにマッチするリクエストに対して Exposed Credentials Check の Custom Ruleset（黒枠相当）が呼び出されます。 


```shell:
$ curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/rulesets/phases/http_request_firewall_custom/entrypoint" \
--header 'X-Auth-Email: '$EMAIL'' --header 'X-Auth-Key: '$API_KEY'' --header 'Content-Type: application/json' -s -d '@ruleset.deploy' -X PUT
```

応答は下記のとおりです。
```json
{
  "result": {
    "id": "8ff668a4e30b4c80869ae9d7c32ed0f7",
    "name": "default",
    "description": "",
    "source": "firewall_custom",
    "kind": "root",
    "version": "5",
    "rules": [
      {
        "id": "ff86ee0ce7e14a479cf98ea1aa3b7f91",
        "version": "1",
        "action": "execute",
        "expression": "(http.host eq \"www2.oyama.cf\" and http.request.uri.path eq \"/my-login\")",
        "description": "Routing condition for an accont level exposed credentials check",
        "last_updated": "2023-11-04T12:05:07.282139Z",
        "ref": "ff86ee0ce7e14a479cf98ea1aa3b7f91",
        "enabled": true,
        "action_parameters": {
          "id": "ef8cfa59128e4d669d7b90056ca8dab7",
          "version": "latest"
        }
      }
    ],
    "last_updated": "2023-11-04T12:05:07.282139Z",
    "phase": "http_request_firewall_custom"
  },
  "success": true,
  "errors": [],
  "messages": []
}
```
無事に配置できました。


#### 動作確認 Account レベル Custom Ruleset（Exposed Credentials Check）
マッチするリクエストを投げると、ヘッダーに Exposed-Credential-Check: 1 が追加されていることがわかります。

```shell
$ curl -s 'https://www2.oyama.cf/my-login' -X POST  -H 'Content-Type: application/json' --data-raw '{"onamae":"CF_EXPOSED_USERNAME@example.com","passwd":"CF_EXPOSED_PASSWORD"}' | jq '.'
{
  :
  "content-type": "application/json",
  "Exposed-Credential-Check": "1",
  "CDN-Loop": "cloudflare",
  :
}
```

条件にマッチさせないリクエストにすると（onamae を username に変更）、ヘッダーが入りません。

```shell
$ curl -s 'https://www2.oyama.cf/my-login' -X POST  -H 'Content-Type: application/json' --data-raw '{"username":"CF_EXPOSED_USERNAME@example.com","passwd":"CF_EXPOSED_PASSWORD"}' | jq '.'
{
  :
  "content-type": "application/json",
  "CDN-Loop": "cloudflare",
  :
}

```
#### 余談：Zone の Exposed Credentials Check との兼ね合い

[Phase の説明](https://developers.cloudflare.com/ruleset-engine/about/phases/)を見ると各 Phase で Account-level のあとに Zone-level が評価されます。

<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/9d3224d5-26fa-d1b6-cd6f-d2a17b1f2233.png">

なので、exposed_credential_check が http_request_firewall_managed フェーズの Zone レベルで再度評価されると不都合がでてくる場合は Zone での設定は停止し Account の方に寄せるのがいいでしょう。
また、同時稼働させる場合は処理の流れに注意します。
<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/75238b78-95d0-70bf-d033-555424d9dc5d.png">

各 Phase の評価[順序](https://developers.cloudflare.com/ruleset-engine/reference/phases-list/#request-phases)は下記のとおりです（custom のあとに managed）。

|phase|level|Exposed Credentials Check|
|-|-|-|
|http_request_firewall_custom|account|✓|
|http_request_firewall_custom|zone|-なし-|
|http_request_firewall_managed|account|✓|
|http_request_firewall_managed|zone|✓|

また、Exposed Credeintials Check のみ抜き出し、図にしてみました。
```mermaid 
flowchart TD
    A[Eyeball] -->|incoming request| B([phase: http_request_firewall_custom<br>account-level<br>entry point ruleset<br>..rules..])
    subgraph http_request_firewall_custom
    B -->|rule番号n<br>expression: www2.oyama.cf/my-login<br>action: execute 'id: xx'| C[phase: http_request_firewall_custom<br>Ruleset 'id: xx'<br>..rules..]
    C -->|rule番号n <br>expression: 'POST', 'application-json'<br>Exposed Credential Check:<br>username-expression: JSON str onamae<br>password-expression: JSON str passwd<br>action:rewrite<br>header insertion 'Exposed-Credential-Check: 1'| D([phase: http_request_firewall_custom<br>zone-level<br>entry point ruleset<br>..ここは ECC 未対応..])
    end
    subgraph http_request_firewall_managed
    D --> E([phase: http_request_firewall_managed<br>account-level<br>entry point ruleset])
    E --> |必要があれば Account レベルで ECC managed rulesetを展開可能|F([phase: http_request_firewall_managed<br>zone-level<br>entry point ruleset<br>..rules..])
    F -->|rule番号n <br>expression: any<br>action execute 'id: yy'| G[phase:http_request_firewall_managed<br>Ruleset: 'id: yy'<br>expression: any<br>Zone  ECC managed rules<br>..rules..<br>drupal, ghost, etc.<br>]
    end
    G --> Next-phase
```
*備考
Zone レベル 唯一のデフォルト Block ルール である 
> Blocks requests containing 'Exposed-Credential-Check' headers, as these can be used to trick the origin into believing a request contained (or didn't contain) an exposed credential" 

の影響が気になりましたが、Account レベル の Custom Rules で追加した Exposed-Credential-Check ヘッダーは Block の対象とならないようで、止められることはありませんでした。
一方、Eyeball で Exposed-Credential-Check ヘッダーを付けたリクエストは、そのルールでブロックされました。

#### ダッシュボード

API で一連の設定が終わるとダッシュボードの Account level WAF にも設定が表示されましたが、一部反映されていないものもあるので、サポートされるまでは API での操作が無難と思います。

<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/479e7499-4892-e934-0cf8-7cee05b37445.png">
<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/b8d4b1e1-951b-e1d8-3191-bd06db026fe6.png">
<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/389cf1d5-c54f-24d1-246f-ad949aeaefd2.png">
<img width="600" alt="alt" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/d6a00628-3146-b5b4-1112-4a65bd8231ff.png">


#### 遊び
- ヘッダーを変えて Update

Rewrite 時、他のヘッダー名や値は受け入れられるか？

```json:新しいヘッダー
      "action_parameters": {
        "headers": {
          "Ex-Cr-Ch": {
            "operation": "set",
            "value": "111111111111"
          }
        }
      },
```
エラーで失敗
```json:エラー
# Header名は Exposed-Credential-Check だけしけ入れられない
{
  "result": null,
  "success": false,
  "errors": [
    {
      "code": 20139,
      "message": "for rules using 'exposed_credential_check', you may only rewrite the 'Exposed-Credential-Check' header",
      "source": {
        "pointer": "/rules/0/action_parameters/headers"
      }
    }
  ],
  "messages": null
}

# 値も 1 だけしけ入れられない
{
  "result": null,
  "success": false,
  "errors": [
    {
      "code": 20139,
      "message": "for rules using 'exposed_credential_check', you must set the 'value' field to '1'",
      "source": {
        "pointer": "/rules/0/action_parameters/headers"
      }
    }
  ],
  "messages": null
}
```



## 補足
Zone レベルの ruleset

```json:http_request_firewall_managed の zone の entry point ruleset（の中の Exposeed Credentials Check 呼び出し用の Rule）
$ curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/phases/http_request_firewall_managed/entrypoint" \
--header 'X-Auth-Email: '$EMAIL'' --header 'X-Auth-Key: '$API_KEY'' --header 'Content-Type: application/json' -s  | jq '.result.rules[]|select(.action_parameters.id=="'$RULESET_ID'")'
{
  "id": "641f6b64ee1a4003b1bcdee81a9e823f",
  "version": "15",
  "action": "execute",
  "expression": "true",
  "description": "zone",
  "last_updated": "2023-11-05T07:14:32.874846Z",
  "ref": "641f6b64ee1a4003b1bcdee81a9e823f",
  "enabled": true,
  "action_parameters": {
    "id": "c2e184081120413c86c3ab7e14069605",
    "version": "latest",
    "matched_data": {
      "public_key": "*"
    }
  }
}
```

```json:http_request_firewall_managed の zone の ruleset（Exposeed Credentials Check の Ruleset）

$ RULESET_ID=`curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets" \
--header 'X-Auth-Email: '$EMAIL'' --header 'X-Auth-Key: '$API_KEY'' --header 'Content-Type: application/json' -s | jq '.result[]|select (.name|contains ("Expose")).id' -r`

$ curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$RULESET_ID" \
--header 'X-Auth-Email: '$EMAIL'' --header 'X-Auth-Key: '$API_KEY'' --header 'Content-Type: application/json' -s
{
  "result": {
    "id": "c2e184081120413c86c3ab7e14069605",
    "name": "Cloudflare Exposed Credentials Check Ruleset",
    "description": "Exposed credentials check rules",
    "source": "firewall_managed",
    "kind": "managed",
    "version": "86",
    "rules": [
      {
        "id": "ef21b0a932ae422790f9249d213b85e6",
        "version": "83",
        "action": "block",
        "categories": [
          "header"
        ],
        "description": "Blocks requests containing 'Exposed-Credential-Check' headers, as these can be used to trick the origin into believing a request contained (or didn't contain) an exposed credential",
        "last_updated": "2023-06-15T17:59:49.614653Z",
        "ref": "2be7739490d648e8f217d5cf02caf47c",
        "enabled": true
      },
      :
      :
```

## まとめ

Exposed Credeintials Check の設定について書きました。
API での設定が必須だったため Ruleset の記載が増えてしいましたが、そちらは改めて書こうと思います。

## 参考リンク
https://migp.cloudflare.com/
https://blog.cloudflare.com/privacy-preserving-compromised-credential-checking/
https://blog.cloudflare.com/account-takeover-protection/
https://developers.cloudflare.com/waf/managed-rules/reference/exposed-credentials-check/
https://developers.cloudflare.com/ruleset-engine/reference/phases-list/
https://haveibeenpwned.com/