はじめに
Cloudflare でオリジンサーバーを保護し、アプリケーションサービスの安定提供を目指すプロダクトの 1 つに Rate Limiting Rules があります。
Eyeball から着信したリクエストを条件によってカウントし、しきい値を超えた場合は抑制し、オリジンサーバーへの過度なアクセスを防ぐ効果が期待できます。
"Rate Limiting Rules" = "Advanced Rate Limiting"
本家ページで使われている用語が若干ややこしいので、プロダクトとその名前の解説を少し。
2022 年に Rate Limitng Rules が出てきたのですが、これは現在の Cloudflare Proxy を支えている Ruleset Engine の上に建つプロダクトの 1 つです。以前は別のエンジンを使う Rate Limitng がありました。
ダッシュボード上では新しい Rate Limiting Rules を Advanced rate limiting、以前のものは Previous version と区別しています。(API も違います)
Advanced
API はこちら(Rulesets 共通のエンドポイント)。
Ruleset Engine の phase は http_ratelimit になります。
Previous
API はこちら。
Rate Limiting Rules 動作イメージ
ざっくり書くと、レートリミットの候補となったリクエストは指定した条件によってクラス分けされます。そのクラスでリクエストのカウンターがしきい値を超過すると、後続のリクエストはレートリミットのアクションを取られます。
リクエストをレートリミット候補にするか、どういう場合にレートリミットをかけるか、どういう場合にカウンターを加算するか、これらの条件を別に指定できることが特徴の一つになります。
入力マッチルール(If incoming requests match...)
着信したリクエストをレートリミット候補にするかの条件定義です。
(http.host eq "ec2.oyama.cf")
と指定してます。
この場合、host にマッチしたものがレートリミットの対象になります。
入力マッチに使える Field は後述します。
また、Cache status
をチェックすると、キャッシュ済みのアセットもレートリミットの対象とすることができます。
特徴でクラス分け(With the same characteristics...)
リクエストを区別しクラス分けすることで、レートリミットの適用範囲を限定します。
この場合、送信元の IP アドレスごとにクラスを分けます。クラスごとにカウンター値が加算されます。
なお、Rate Limiting Rules は Cloudflare のデーターセンター(Colo)ごとに処理されます。(データセンターを超えて協働するわけではありません)
The Cloudflare data center ID (cf.colo.id) is a mandatory characteristic of every rate limiting rule to ensure that counters are not shared across data centers. This characteristic does not appear in the rule configuration in the dashboard, but you must include it when creating rate limiting rules via API.
With the same characteristics...
には cf.colo.id
が暗黙の条件として定義されています。(API で設定を取ると指定されていることがわかります)
送信元 IP が同じ場合、リクエストの到達するデータセンターが変わることはほぼないので、分別に影響はなさそうです。
なお Eyeball が接続している Colo は /cdn-cgi/trace で確認できます。
#東京から
~$ curl -s https://ec2.oyama.cf/cdn-cgi/trace|grep colo
colo=NRT
#ブラジルから
~$ curl -s https://ec2.oyama.cf/cdn-cgi/trace|grep colo
colo=GRU
リクエストの特徴分けに使える Field は後述します。
また Use custom counting expression
をクリックすると、カウント条件を変更することができます。
カウント条件
カウント条件のデフォルトは入力マッチルールです。
ここでは下記になります。
(http.host eq "ec2.oyama.cf")
つまり
-
(http.host eq "ec2.oyama.cf")
をレートリミットの対象にして -
cf.colo.id と ip.src (AND 条件)
でクラスを分けて -
(http.host eq "ec2.oyama.cf")
であれば、カウントする
というルールになります。
カウント条件を変更する例は後述します。
レートリミット(When rate exceeds...)
表示のままです。
10 秒間に 5 リクエストを超えたら... となります。
測定間隔は下記が指定できます。(プランによります)
アクション(Then take action...)
レートを超えた場合に、そのクラスに対するアクションを指定します。
この場合、Block で text メッセージを返しています。
使えるアクションは後述します。
また、全リクエストを一定期間対象にするか、超過分を対象にするかを選ぶことができます。
適正なレートを探る
2023 年 9 月の Blog のアップデートにありましたが、Security Analytics のダッシュボードから IP や JA3 Fingerprint でリクエストの統計を分析する機能が付きました(Enterprise プラン)。
期間中のリクエストを任意のフィルタで抽出し、定義済みの条件(IP、JA3 または IP + JA3)でクラス化、それぞれの単位時間あたりのリクエスト数を確認可能です。
例えば、右ペインで Bot Score 20 以下を選ぶと、その部分だけが抜き出されます。
単位時間の Bot リクエストを把握できるため Bot 制限のためのレート戦略が立てられます。
中締め
上記の Rate Limiting ルールを図に落とすと、下記になります。
API と対比してみます。
{
"id": "6ab99a1a40c543598a9606cbac5c5a73",
"name": "default",
"description": "",
"source": "rate_limit",
"kind": "zone",
"version": "41",
"rules": [
{
"id": "e0dd26df949d41bb868b37f445f23b97",
"version": "9",
"action": "block",
"ratelimit": {
"characteristics": [ <--- クラス分け
"ip.src",
"cf.colo.id"
],
"period": 10,
"requests_per_period": 5,
"mitigation_timeout": 10
},
"expression": "(http.host eq \"ec2.oyama.cf\")", <--- 入力マッチ, カウント
"description": "adrl test",
"last_updated": "2023-10-07T06:48:46.295931Z",
"ref": "e0dd26df949d41bb868b37f445f23b97",
"enabled": true,
"action_parameters": { <--- アクション
"response": {
"status_code": 429,
"content": "b",
"content_type": "text/plain"
}
}
},
動作の例
同じ IP からのリクエスト
同じ IP の 2 つのターミナルから 1 秒おきにリクエストを送ってますが、左は 2 個、右は 3 個(合計 5 )でブロックが開始されました。
リクエストが同じクラスに分類されていることがわかります。
違う IP からのリクエスト
違う IP からリクエストを送ります。
お互いにおおよそ 5 リクエスト程度でブロックが開始されました。
リクエストが異なるクラスに分類されていることがわかります。
カウント条件の変更(同じ IP からの通信を他の条件でクラス分け)
Mac とその上の仮想マシンからリクエストをしていますが、同じ IP を使う環境となっています。
# Mac
~ $ curl -V
curl 8.2.1 (x86_64-apple-darwin22.4.0)
# 仮想マシン
keio@vm1:~$ curl -V
curl 7.81.0 (x86_64-pc-linux-gnu)
何かしら区別できるか、Cloudflare の Analytics からドリルダウンしてみます。
User Agent あるいは JA3 Fingerprint(Bot Management 契約の Enterprise プラン)に 2 種類出てきています。
これらを条件に入れることで、同じ IP でもリクエストを区別ができそうです。
User Agent は簡単に変更できるので、JA3 Fingerprint を使ってクラス分けをしようと思います。JA3 Finterprint の 014 始まりが Mac curl の通信です。
レートリミットの設定は、クラス分けに JA3 Fingerprint を追加、Mac curl のときのみカウントし、アクションに当たるようにします。
"ratelimit": {
"characteristics": [ <--- クラス分け
"ip.src",
"cf.bot_management.ja3_hash",
"cf.colo.id"
],
"period": 10,
"requests_per_period": 5,
"mitigation_timeout": 10,
"counting_expression": "(cf.bot_management.ja3_hash eq \"0149f47eabf9a20d0893e2a44e5a6323\")" <--- カウント
},
クラス分けおよびカウント条件をカスタムしたことで、下記の動作に変わります。
カウントに使える条件は後述しますが、入力マッチの条件との違いはレスポンスを見れることです。
レスポンスを条件に
カウンターの加算には Eyeball からのリクエストだけでなく、オリジンサーバーからのレスポンス(ヘッダー・ステータスコード)を指定することができます。
オリジンサーバーの負荷などをもとにレートリミットしたい場合に使えます。
同じホストからのリクエストでもオリジンサーバーからのステータスコードが 200 の場合通過しますが、500 の戻りがしきい値を超えると Block が発動しています(429)。
#ステータスコード 200 レートリミットなし
~ $ while true; do curl "https://www2.oyama.cf/?code=200" -svo /dev/null 2>&1 | grep "< HTTP/2"; sleep 1; done
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
< HTTP/2 200
#ステータスコード 500 レートリミットあり
~ $ while true; do curl "https://www2.oyama.cf/?code=500" -svo /dev/null 2>&1 | grep "< HTTP/2"; sleep 1; done
< HTTP/2 500
< HTTP/2 500
< HTTP/2 500
< HTTP/2 500
< HTTP/2 500
< HTTP/2 500
< HTTP/2 500
< HTTP/2 429 <‐‐‐ レートリミット開始
< HTTP/2 429
< HTTP/2 429
< HTTP/2 429
< HTTP/2 429
< HTTP/2 429
< HTTP/2 429
< HTTP/2 429
< HTTP/2 500 <‐‐‐ レートリミット解除
< HTTP/2 500
< HTTP/2 500
< HTTP/2 500
< HTTP/2 500
< HTTP/2 500
< HTTP/2 500
< HTTP/2 429
< HTTP/2 429
まとめ
アプリケーションへのリクエスト着信をレートという観点で分類・分析し、オリジンサーバーへ流入をコントロールすることができるツールだと思います。
どのような使いようができるか、サービス提供側でしか思いつかないアイデアもあるかと思うので、ぜひ工夫いただければと思います。
補足
ユースケース・ベストプラクティス
環境や目的に合わせて柔軟に構成ができるツールです。
本家のサンプルにアイデアの元があるかもしれません。
プラン別の比較
Enterprise プランで試しました。
Rate Limiting 機能のプラン別の比較はこちらです。
IP with NAT support って何?
Enterprise プランの場合、特徴でのクラス分けに IP with NAT support ってのがあります。単なる IP と何が違うんでしょうか。
Use IP with NAT support to handle situations such as requests under NAT sharing the same IP address. Cloudflare uses a variety of privacy-preserving techniques to identify unique visitors, which may include use of session cookies. Refer to Cloudflare Cookies for details.
CGNAT などの IP 共有状態でもなるべく Eyeball を区別しようとする試みで、HTTP ヘッダーが使われるようです。
特徴を IP から IP with NAT support に変えてみます。
"ratelimit": {
"characteristics": [
"cf.unique_visitor_id", <--- IP with NAT support
"cf.colo.id"
],
すると、下記のようにレスポンスに新しい set-cookie が付与され始めます。
この _cfuvid が Rate Limiting で NAT を意識した区別のもとになるようです。
< set-cookie: _cfuvid=BOKA7bH54GOUXXSD1KbDv16H9euO6x_nkBh3IkJf650-1696666988432-0-604800000; path=/; domain=.oyama.cf; HttpOnly; Secure; SameSite=None
同じ IP からの通信の片方に _cfuvid Cookie を付けてみると、リクエストが独立して処理されます。Cookie を認識して別のクラスに分けられていますね。
ただ、Cookie をつけてこない Eyeball はこれでは区別ができないですね。
Rate Limiting (previous version) の場合は _cfruid となります。
入力マッチに使える Expression Field
入力マッチ If incoming requests match...
のフィルターに指定できる Fields は GUI のビルダー に出てきたものだと下記あたりでした。
- 送信元 IP 関連
- HTTP ヘッダー・ボディ関連
- Bot スコア(Bot management 有効時)
- 脅威スコア(IP Reputation)
- WAF attack スコア
契約サービスにより増減があるので、あくまで参考まで。
オペレーターは適当につけています。
下記のルール、一行一行を AND OR で組み合わせることができます。
ip.geoip.country in {"JP" "KR"}
ip.geoip.continent eq "AS"
ip.geoip.asnum eq 1
ip.src in {192.0.2.1 192.0.2.2}
ssl
http.cookie eq "name=value"
http.host contains "ec2"
http.referer contains "example"
http.request.method in {"GET" "PUT"}
not http.request.version in {"HTTP/1.0"}
http.request.full_uri contains "https://example.com/contact?page=1234"
http.request.uri contains "/content?page=1234"
http.request.uri.path eq "/content"
starts_with(http.request.uri.query, "page=1234")
http.user_agent matches ".*MSIE [8-9].*"
all(http.request.headers["x-hoge"][*] ne "moge")
http.request.body.size lt 100
http.request.body.mime eq " image/jpeg"
http.request.body.raw matches "^a"
lookup_json_string(http.request.body.raw, "key") eq "value"
http.x_forwarded_for eq "192.0.2.1"
cf.tls_client_auth.cert_verified
not cf.client.bot
cf.bot_management.score le 20
cf.bot_management.ja3_hash eq "df669e7ea913f1ac0c0cce9a201a2ec1"
not cf.bot_management.verified_bot
not cf.bot_management.static_resource
cf.threat_score lt 20
cf.waf.score le 10
cf.waf.score.rce eq 5
cf.waf.score.sqli eq 5
cf.waf.score.xss eq 5
特徴識別に使える Expression Field
リクエストの特徴でクラスを分ける With the same characteristics...
では下記が指定できます。
[
{
"Dashboard value": "N/A (implicitly included)",
"API value": "cf.colo.id (mandatory)"
},
{
"Dashboard value": "IP",
"API value": "ip.src"
},
{
"Dashboard value": "IP with NAT support",
"API value": "cf.unique_visitor_id"
},
{
"Dashboard value": "Header value of (enter header name)",
"API value": "http.request.headers[\"<header_name>\"]"
},
{
"Dashboard value": "Cookie value of (enter cookie name)",
"API value": "http.request.cookies[\"<cookie_name>\"]"
},
{
"Dashboard value": "Query value of (enter parameter name)",
"API value": "http.request.uri.args[\"<query_param_name>\"]"
},
{
"Dashboard value": "Host",
"API value": "http.host"
},
{
"Dashboard value": "Path",
"API value": "http.request.uri.path"
},
{
"Dashboard value": "AS Num",
"API value": "ip.geoip.asnum"
},
{
"Dashboard value": "Country",
"API value": "ip.geoip.country"
},
{
"Dashboard value": "JA3 Fingerprint",
"API value": "cf.bot_management.ja3_hash"
},
{
"Dashboard value": "JSON string value of (enter key)",
"API value": "lookup_json_string(http.request.body.raw, \"<key>\")"
},
{
"Dashboard value": "N/A (API only)",
"API value": "lookup_json_integer(http.request.body.raw, \"<key>\")"
},
{
"Dashboard value": "Body",
"API value": "http.request.body.raw"
},
{
"Dashboard value": "Body size (select operator, enter size)",
"API value": "http.request.body.size"
},
{
"Dashboard value": "Form input value of (enter field name)",
"API value": "http.request.body.form[\"<input_field_name>\"]"
},
{
"Dashboard value": "N/A (API only)",
"API value": "substring(<field>, <start>[, <end>])"
}
]
カウント条件に使える Expression Field
カウント条件をカスタム Increment counter when…
する場合、入力マッチで利用できる Fields に加えて、レスポンスが追加されています。
(any(http.response.headers["x-hoge"][*] eq "moge")
http.response.code eq 500)
上述の通り、デフォルトは入力マッチ条件がそのままカウント条件になります。
アクションの種類
使えるアクションは下記の通りです。Block についてはオプションがあります。
Rulesets 全体のアクションの説明はこちら。
設定の適用ポイント
Rate Limiting Rules を Zone に適用しましたが、Account にも適用可能です。
オリジンサーバーが API を提供する場合
API Shield というプロダクトは機械学習によって API エンドポイントのレートを学習し、この Rate Limiting Rules を利用します。
Bot Managment など他のプロダクトとの連携で API をセキュアに保ちます。