[AWS CDK] AWS WAFで同一のIPアドレスから同一のURIパスに複数回アクセスがあった場合にブロックするレートベースルールを設定してみた

[AWS CDK] AWS WAFで同一のIPアドレスから同一のURIパスに複数回アクセスがあった場合にブロックするレートベースルールを設定してみた

URIパスによってレート閾値を調整しよう
Clock Icon2025.02.25

送信元IPアドレスのみの判定ではなく、同一URIパスに何回アクセスしたのかでブロックしたい

こんにちは、のんピ(@non____97)です。

皆さんはWAFの運用をしていて、送信元IPアドレスのみの判定ではなく、同一URIパスに何回アクセスしたのかでブロックしたいなと思ったことはありますか? 私はあります。

よく「送信元IPアドレス単位で指定した閾値以上のアクセス回数があった場合に期間中ブロックする」というレートベースルールを設定することがあると思います。

ただし、レートの閾値でギリギリを攻めてしまうと通常ユーザーからのアクセスもブロックしてしまう可能性があります。

特に、大量の画像やCSS、JavaScriptを読み込むようなページがある場合、簡単に閾値を超過してしまうでしょう。

そんな時、同一IPアドレスが、同一URIパスにアクセスした回数のレートベースルールを設定したいところです。

以下アップデートにてURIパスをレートベースルールの集約キーに設定できるようになりました。

https://dev.classmethod.jp/articles/aws-waf-rate-base-rule-support-uri-path-aggregation-key/

これを活用することで、同一のIPアドレスから同一のURIパスへのアクセス回数に対するレートベースルールを簡単に実現することが可能です。

今回はAWS CDKで実際に設定してみました。

検証環境

検証環境は以下のとおりです。

[AWS CDK] AWS WAFで同一のIPアドレスから同一のURIパスに複数回アクセスがあった場合にブロックするレートベースルールを設定してみた検証環境構成図.png

検証環境は全てAWS CDKでデプロイしました。使用したコードは以下GitHubリポジトリに保存しています。

https://github.com/non-97/cloudfront-s3-website/tree/v5.0.0

こちらのベースとなったコードの詳細な説明は以下記事をご覧ください。

https://dev.classmethod.jp/articles/aws-cdk-cloudfront-s3-website/

https://dev.classmethod.jp/articles/aws-cdk-cloudfront-s3-website-log-analytics/

https://dev.classmethod.jp/articles/cloudfront-s3-website-webp-image-delivery/

AWS WAF周りのコードの紹介

WebACLでは以下のルールを用意しました。

  • 閾値 : 60秒間に10回
  • アクション : ブロック
  • 集約キー : URIパス および 送信元IPアドレス
  • スコープダウンポリシー : URIパスの末尾が/で終了する または URIパスが空

スコープダウンポリシーで「URIパスの末尾が/で終了する または URIパスが空」としたのは、画像やJavaScriptファイルなどの静的コンテンツを含めないようにするためです。

今回は「URIパスの末尾が/で終了する または URIパスが空の場合は、CloudFrontでキャッシュは行わず、毎回オリジンに問い合わせが走る」というシナリオで考えています。(今回の検証環境はオリジンはS3で動的なコンテンツはなし)

それにマッチしない = CloudFrontでキャッシュされているものについては、キャッシュが効いている限りオリジンに到達はしません。

もし、AWS WAFの導入のモチベーションが大量アクセス時のオリジンへの負荷対策のみであれば、厳しいレートリミットはかけなくても良いのではと考えています。

レートベースルールの具体的なコードは以下のとおりです。

./lib/construct/waf-construct.ts
        {
          name: `RateLimit_SameIPSameURI`,
          priority: 10,
          action: { block: {} },
          statement: {
            rateBasedStatement: {
              limit: 10,
              aggregateKeyType: "CUSTOM_KEYS",
              evaluationWindowSec: 60,
              customKeys: [
                {
                  uriPath: {
                    textTransformations: [
                      {
                        type: "NONE",
                        priority: 0,
                      },
                    ],
                  },
                },
                {
                  ip: {},
                },
              ],
              scopeDownStatement: {
                regexMatchStatement: {
                  fieldToMatch: {
                    uriPath: {},
                  },
                  textTransformations: [
                    {
                      type: "NONE",
                      priority: 0,
                    },
                  ],
                  regexString: ".*/$|^$",
                },
              },
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: "RateLimit_SameIPSameURI",
          },
          ruleLabels: [
            {
              name: "RateLimit_SameIPSameURI",
            },
          ],
        },

また、以下のマネージドルールを追加しています。

  • AWSManagedRulesCommonRuleSet
  • AWSManagedRulesKnownBadInputsRuleSet
  • AWSManagedRulesAmazonIpReputationList
  • AWSManagedRulesAnonymousIpList

AWS WAFのConstruct全体のコードは以下のとおりです。

https://github.com/non-97/cloudfront-s3-website/blob/v5.0.0/lib/construct/waf-construct.ts

動作確認

/ に複数回アクセス

動作確認をします。

まず、/に複数回アクセスします。

$ date 
Tue Feb 25 02:42:46 AM UTC 2025

$ curl https://checkip.amazonaws.com/
3.235.165.215

$ for i in {1..100}; do 
    echo -n "Request ${i}: "
    curl -s \
      -o /dev/null \
      -w "%{http_code}\n" \
      https://www.non-97.net/
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 28: 200
Request 29: 200
Request 30: 200

はい、30回ほどアクセスしましたが、まだブロックされません。

もう一度トライします。

$ for i in {1..100}; do 
    echo -n "Request ${i}: "
    curl -s \
      -o /dev/null \
      -w "%{http_code}\n" \
      https://www.non-97.net/
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 28: 200
Request 29: 200
Request 30: 200

まだ変わりませんね。

$ for i in {1..30}; do 
    echo -n "Request ${i}: "
    curl -s \
      -o /dev/null \
      -w "%{http_code}\n" \
      https://www.non-97.net/
  done
Request 1: 403
Request 2: 403
Request 3: 403
.
.
(中略)
.
.
Request 28: 403
Request 29: 403
Request 30: 403

はい、ブロックされました。

実際にブロックされるまでは遅延が存在しません。

ドキュメントには通常30秒未満の遅延があると記載がありました。

AWS WAF がリクエストのレートを推定するたびに、AWS WAF は設定された評価ウィンドウ内に受信したリクエストの数を遡って確認します。これや伝播遅延などの要因により、AWS WAF がリクエストを検出してレート制限を適用するまでに、数分間にわたってリクエストが過剰なレートで到着する可能性があります。同様に、リクエスト率が一定期間制限以下になると、AWS WAF がその減少を検出してレート制限のアクションを停止するまで時間がかかることがあります。通常、この遅延は 30 秒未満です。

AWS WAF でのレートベースのルールの規制 - AWS WAF、 AWS Firewall Manager、および AWS Shield Advanced

/ に異なるIPアドレスで複数回アクセス

それでは、/に異なるIPアドレスから複数回アクセスします。

$ date
Tue Feb 25 02:43:52 AM UTC 2025

$ curl https://checkip.amazonaws.com/
43.207.178.73

$ for i in {1..100}; do 
    echo -n "Request ${i}: "
    curl -s \
      -o /dev/null \
      -w "%{http_code}\n" \
      https://www.non-97.net/
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 98: 200
Request 99: 200
Request 100: 200

$ for i in {1..100}; do 
    echo -n "Request ${i}: "
    curl -s \
      -o /dev/null \
      -w "%{http_code}\n" \
      https://www.non-97.net/
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 82: 200
Request 83: 200
Request 84: 200
Request 85: 403
Request 86: 403
Request 87: 403
.
.
(中略)
.
.
Request 98: 403
Request 99: 403
Request 100: 403

$ date
Tue Feb 25 02:44:18 AM UTC 2025

はい、先ほどの実行から送信元IPアドレスを変更して再度アクセスするまで1分も間隔は空いていませんが、最初の頃はブロックされずに正常にアクセスできました。そして、途中からブロックされるようになることが分かりました。

URIパスを指定せずに複数回アクセス

URIパスを指定せずに複数回アクセスしてみます。

IPアドレスは初回に実行したときのものを使用しており、初回実行時から2分以上空いています。

$ date
Tue Feb 25 02:45:22 AM UTC 2025

$ curl https://checkip.amazonaws.com/
3.235.165.215
$ for i in {1..100}; do 
    echo -n "Request ${i}: "
    curl -s \
      -o /dev/null \
      -w "%{http_code}\n" \
      https://www.non-97.net
    sleep 1
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 22: 200
Request 23: 200
Request 24: 200
Request 25: 403
Request 26: 403
Request 27: 403
.
.
(中略)
.
.
Request 40: 403
Request 41: 403
Request 42: 403
^C

$ date
Tue Feb 25 02:46:17 AM UTC 2025

最初は200が返ってき、レートを超過してから20秒ほどでブロックされるようになりました。

異なるIPアドレスでURIパスを指定せずに複数回アクセス

異なるIPアドレスでURIパスを指定せずに複数回アクセスします。

先ほどの実行完了してから15秒ほどしか経過していません。

$ date 
Tue Feb 25 02:46:31 AM UTC 2025

$ curl https://checkip.amazonaws.com/
43.207.178.73

$ for i in {1..100}; do 
    echo -n "Request ${i}: "
    curl -s \
      -o /dev/null \
      -w "%{http_code}\n" \
      https://www.non-97.net
    sleep 1
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 31: 200
Request 32: 200
Request 33: 200
Request 34: 403
Request 35: 403
Request 36: 403
Request 37: 403
Request 38: 403
Request 39: 403
Request 40: 403
Request 41: 403
^C

$ date
Tue Feb 25 02:47:18 AM UTC 2025

はい、15秒ほどしか経過していないにも関わらず、最初はブロックされずに正常にアクセスできました。

/dir/ に複数回アクセス

続いて、/dir/に複数回アクセスします。

$ date
Tue Feb 25 02:48:10 AM UTC 2025

$ curl https://checkip.amazonaws.com/
3.235.165.215

$ for i in {1..100}; do 
    echo -n "Request ${i}: "
    curl -s \
      -o /dev/null \
      -w "%{http_code}\n" \
      https://www.non-97.net/dir/
      sleep 1
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 20: 200
Request 21: 200
Request 22: 200
Request 23: 200
Request 24: 403
Request 25: 403
Request 26: 403
.
.
(中略)
.
.
Request 40: 403
Request 41: 403
Request 42: 403
^C

$ date
Tue Feb 25 02:49:10 AM UTC 2025

こちらも問題なく動作しています。

/dir/ に異なるIPアドレスで複数回アクセス

/dir/に異なるIPアドレスで複数回アクセスします。

$ date
Tue Feb 25 02:49:15 AM UTC 2025

$ curl https://checkip.amazonaws.com/
43.207.178.73

$ for i in {1..100}; do 
    echo -n "Request ${i}: "
    curl -s \
      -o /dev/null \
      -w "%{http_code}\n" \
      https://www.non-97.net/dir/
      sleep 1
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 27: 200
Request 28: 200
Request 29: 200
Request 30: 403
Request 31: 403
Request 32: 403
.
.
(中略)
.
.
Request 39: 403
Request 40: 403
Request 41: 403
^C

$ date
Tue Feb 25 02:50:03 AM UTC 2025

はい、15秒ほどしか経過していないにも関わらず、最初はブロックされずに正常にアクセスできました。

/non__97.png に複数回アクセス

/non__97.pngに複数回アクセスしてみます。

$ date
Tue Feb 25 02:50:46 AM UTC 2025

$ curl https://checkip.amazonaws.com/
3.235.165.215

$ for i in {1..100}; do 
  echo -n "Request ${i}: "
  curl -s \
    -o /dev/null \
    -w "%{http_code}\n" \
    https://www.non-97.net/non__97.png
    sleep 1
done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 80: 200
Request 81: 200
Request 82: 200
^C
$ date
Tue Feb 25 02:52:24 AM UTC 2025

はい、1分間に60回ほど実行しましたが、ブロックされませんでした。

正規表現のパターンとマッチしないので意図したとおりです。

Top Insightsセクションの確認

ここまでの実行結果をTop Insightsセクションから確認してみましょう。

Top Insightsセクションの詳細は以下記事をご覧ください。

https://dev.classmethod.jp/articles/waf-console-top-insights-visualizations/

内容は以下のとおりです。

1.Top Insights.png

2つのIPアドレスから/もしくは/dir/にGETでアクセスがあったことが分かります。

URIパスを指定しない場合は/と判定されるようですね。

また、その他のダッシュボードも確認してみます。

2.Action summary for the specified time range - All traffic.png

レートベースルールでブロックされていることが分かりますね。

get-rate-based-statement-managed-keysにて、レートベースルールでブロックされているIPアドレスを確認してみます。

$ date
Tue Feb 25 02:53:25 AM UTC 2025

$ aws wafv2 get-rate-based-statement-managed-keys \
    --scope CLOUDFRONT \
    --web-acl-name website \
    --web-acl-id 84a704a7-6965-44af-887e-d196dcd881ac \
    --rule-name RateLimit_SameIPSameURI

An error occurred (WAFUnsupportedAggregateKeyTypeException) when calling the GetRateBasedStatementManagedKeys operation: The rule that you've named doesn't aggregate solely on the IP address or solely on the forwarded IP address. This call is only available for rate-based rules with an AggregateKeyType setting of IP or FORWARDED_IP.

はい、どうやら集約キーがIPFORWARDED_IPでなければ、現在ブロックされているIPアドレスは確認できないようです。

/dir/ にアクセスしてブロックされたら / にアクセスする

先ほどの検証では同一IPアドレスがブロックされてから別URIパスにアクセスするまで数分間間が空いていました。

確かに各URIパスごとにカウントされることを確認するために、/dir/にアクセスしてブロックされたら即座に/にアクセスするようにしてみます。

$ curl https://checkip.amazonaws.com/
3.90.200.160

$ for i in {1..100}; do 
    echo -n "Request ${i}: "
    status=$(curl -s -o /dev/null -w "%{http_code}" https://www.non-97.net/dir/)
    echo $status
    if [ "$status" -eq 403 ]; then
      date
      break
    fi
    sleep 1
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 25: 200
Request 26: 200
Request 27: 403
Tue Feb 25 04:21:39 AM UTC 2025

$ date
Tue Feb 25 04:21:41 AM UTC 2025

$ curl https://checkip.amazonaws.com/
3.90.200.160

$ for i in {1..100}; do 
  echo -n "Request ${i}: "
  status=$(curl -s -o /dev/null -w "%{http_code}" https://www.non-97.net/)
  echo $status
  if [ "$status" -eq 403 ]; then
    date
    break
  fi
  sleep 1
done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 23: 200
Request 24: 200
Request 25: 403
Tue Feb 25 04:22:08 AM UTC 2025

はい、数秒ほどしか差はありませんが、/にアクセスを開始してしばらくはブロックされていませんでした。

確かにURIパス単位で集約されていることが分かります。

末尾に / が付与されている もしくは 末尾にドット + 拡張子パターンを含まないパス全体の場合

ふと、末尾に/がない場合の挙動が気になりました。

現在、CloudFront Functionsで、末尾が/または、URIパス内に.を含まない場合、末尾に/index.htmlを付与するようにしています。

つまり、/dirにアクセスすると、/dir/index.htmlを返すようにしています。

この場合、/dirに大量にアクセスするとどうでしょうか。

$ date
Tue Feb 25 04:23:03 AM UTC 2025

$ curl https://checkip.amazonaws.com/
3.90.200.160

$ for i in {1..100}; do
    echo -n "Request ${i}: "
    status=$(curl -s -o /dev/null -w "%{http_code}" https://www.non-97.net/dir)
    echo $status
    if [ "$status" -eq 403 ]; then
      date
      break
    fi
    sleep 1
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 72: 200
Request 73: 200
Request 74: 200
^C
$ date
Tue Feb 25 04:24:27 AM UTC 2025

はい、レート以上のアクセスをしてもブロックされませんでした。

/dirにアクセスしてきたら/dir/にリダイレクト or リライトする」というように、トレイリングスラッシュ有無のケアをする対応をしている場合、/dirにアクセスがあった場合もレートベースルールを適用させたいです。

今回は正規表現を.*/$|^[^.]*$|^.*[^.]/[^.]*$としました。

|で分解すると、各パターンの意味は以下のとおりです。

  • .*/$ : 末尾が/のURIパス
  • ^[^.]*$ : .を全く含まないURIパス
  • ^.*[^.]/[^.]*$ : 最後のセグメントに.を含まないURIパス

この正規表現にマッチするようなURIパスは以下のとおりです。

  • (空) : ^[^.]*$
  • / : .*/$
  • /dir/ : .*/$
  • /dir : ^.*[^.]/[^.]*$
  • /api/users : ^[^.]*$ および ^.*[^.]/[^.]*$
  • /api/v1.0/user : ^.*[^.]/[^.]*$

一方で以下のようなURIパスにはマッチしません。

  • /index.html
  • /dir/index.html
  • /image/image.png.webp

AWS CDK側でも設定しておきます。

./lib/construct/waf-construct.ts
              scopeDownStatement: {
                regexMatchStatement: {
                  fieldToMatch: {
                    uriPath: {},
                  },
                  textTransformations: [
                    {
                      type: "NONE",
                      priority: 0,
                    },
                  ],
-                 regexString: ".*/$|^$",
+                 regexString: ".*/$|^[^.]*$|^.*[^.]/[^.]*$",
                },
              },

ちなみにこちらのルールのWCUは66です。

npx cdk deploy後、再度/dirにアクセスしてみます。

$ date
Tue Feb 25 04:55:19 AM UTC 2025

$ curl https://checkip.amazonaws.com/
3.90.200.160

$ for i in {1..100}; do
    echo -n "Request ${i}: "
    status=$(curl -s -o /dev/null -w "%{http_code}" https://www.non-97.net/dir)
    echo $status
    if [ "$status" -eq 403 ]; then
      date
      break
    fi
    sleep 1
  done
Request 1: 200
Request 2: 200
Request 3: 200
.
.
(中略)
.
.
Request 26: 200
Request 27: 200
Request 28: 403
Tue Feb 25 04:55:48 AM UTC 2025

はい、今度はブロックされるようになりました。正規表現様様です。

なお、AWS WAFではサポートされていない正規表現パターンもあります。注意しましょう。

AWS WAF は、PCRE ライブラリ libpcre で使用される正規表現パターン構文をサポートします。ライブラリは、「PCRE - Perl Compatible Regular Expressions」で文書化されています。

AWS WAF は、ライブラリのすべての構成をサポートしているわけではありません。例えば、一部のゼロ幅アサーションをサポートしますが、すべてではありません。サポートされているコンストラクトの包括的なリストはありません。ただし、無効な正規表現パターンを指定した場合、またはサポートされていない設定を使用した場合、AWS WAF API は失敗をレポートします。
AWS WAF は、次の PCRE パターンをサポートしていません。

  • 後方参照と部分式取得
  • サブルーチン参照と再帰パターン
  • 条件付きパターン
  • バックトラック制御動詞
  • \C シングルバイトディレクティブ
  • \R 改行一致ディレクティブ
  • \K 一致開始位置リセットディレクティブ
  • コールアウトと埋め込みコード
  • アトミックグループと所有格量指定子

サブルーチン

また、RegexMatchStatementで指定できる正規表現パターンの長さは512文字です。あまりにも複雑な場合は別ルールに分けたり、正規表現セットを使用しましょう。

RegexString

The string representing the regular expression.

Type: String

Length Constraints: Minimum length of 1. Maximum length of 512.

Pattern: .*

Required: Yes

RegexMatchStatement - AWS WAFV2

URIパスによってレート閾値を調整しよう

AWS CDKを使ってAWS WAFで同一のIPアドレスから同一のURIパスに複数回アクセスがあった場合にブロックするレートベースルールを設定してみました。

URIパスによってレートの閾値を調整してみましょう。

静的なコンテンツと動的なコンテンツが入り混じっている場合は、一律なレートベースルールのみでは対応が難しい場合もあると思います。

特にサインインページや決済ページなど重たい処理を行うコンテンツを返すURIパスについては、厳し目なレートを設定する形が良いと考えます。

この記事が誰かの助けになれば幸いです。

以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.