AWS WAF でカスタムルールグループを作成し、挙動を確認しつつログを CloudWatch Logs で確認した

2024.03.13

コーヒーが好きな emi です。

AWS WAF をしっかり触ったことがなかったのと、WAF のログがどんな感じで出力されるのか確認したかったので、検証してみました。 AWS WAF でカスタムルールグループを作成し、挙動を確認しつつログを CloudWatch Logs で確認していきます。

構成図

今回の検証の構成図は以下のようなイメージです。

なぜ検証でカスタムルールグループを作成するのか?

AWS WAF ではデフォルトでそのまま使えるマネージドルールグループが提供されています。「Anonymous IP list(匿名 IP リスト)」や「Core rule set(コアルールセット)」、「Known bad inputs(既知の悪意のある入力)」などのマネージドルールグループを使うことで、セキュリティの専門知識がなくても、一般的な Web 脅威から素早く手軽にアプリケーションを保護できるというメリットがあります。

一方で、「アクセスが想定通り block される、count される」などの挙動を確認するには、マネージドルールグループのルールに引っ掛けるために匿名通信をおこなうソフトウェアを導入したり、既知の不正な入力をおこなったりする必要があります。これはサッと済ませたい PoC や技術検証には不向きです。

手軽に WAF の挙動を確認をおこなうために、例えば特定の IP アドレスからの通信をブロックするなどのカスタムルールをカスタムルールグループに追加することで、手軽に WAF の挙動確認を行うことができるというわけです。

事前準備

VPC、EC2、ALB の作成は以下トグル内を参考に事前におこなっておいてください。

事前準備(クリックで展開)

VPC、EC2 の作成

構成図を参考に VPC と EC2 を作成してください。今回は以下の AMI から EC2 インスタンスを作成しました。

  • AMI 名:Amazon Linux 2023 AMI
  • 説明:Amazon Linux 2023 AMI 2023.3.20240304.0 x86_64 HVM kernel-6.1
  • AMI ID:ami-039e8f15ccb15368a

NAT Gateway をたてて Apache HTTP Server がインストールできるようにしました。

セッションマネージャーで EC2 に接続し Apache HTTP Server のインストールと設定

EC2 インスタンスに AmazonSSMManagedInstanceCore 権限をもった IAM ロールを付与しておき、セッションマネージャーで接続します。

以下のコマンドを参考に、Apache HTTP Server のインストールとトップページの設定をおこないます。

# パッケージを最新バージョンに更新
sudo yum update -y
# Apache HTTP Server のインストール
sudo yum install httpd -y
# Apacheの起動
sudo systemctl start httpd
# EC2が再起動した際にApacheも自動起動するように設定
sudo systemctl enable httpd
# Apacheが起動したか確認
sudo systemctl status httpd
# HTML ファイルの格納場所へ移動
cd /var/www/html
# トップページ用ファイル(index.html)作成
sudo touch index.html
# index.html が作成されているか確認
ls 
# HTMLファイルの中身を記述
sudo vi index.html
# i を押下し編集モード(INSERT)に移行し、以下内容を貼り付け
<html lang="ja">
 <head>
  <meta charset="utf-8">
  <title>waf-test-hello-world</title>
 </head>
   <body>
    <h1>Hello world!</h1>
   </body>
</html>
# esc を押下し編集モードを終了する
# :wq を入力して Enter でファイルを保存して終了
# ファイルが変更されているか確認
cat index.html

ALB のターゲットグループと ALB の作成

ALB のターゲットグループを作成し、Apache HTTP Server をインストールした EC2 をターゲットに含めてください。インターネット向けの ALB も作成し、リスナーを設定します。今回は検証のため HTTP:80 で接続します。
リソースマップはこのようになります。

セキュリティグループの設定

セキュリティグループの設定も確認しておきましょう。ALB のセキュリティグループは手元の端末からの HTTP アクセスを許可しておきます。
EC2 インスタンスのセキュリティグループは ALB のセキュリティグループからの HTTP アクセスを許可しておきます。

このように設定し、ALB の DNS 名にブラウザでアクセスすると、「Hello world!」ページが表示されます。

AWS WAF の作成

事前準備ができたら AWS WAF を作成していきます。AWS WAF は

IP Sets

カスタムルールグループとカスタムルール

Web ACL

の順番で作成していきます。

IP Sets の作成

まず IP Sets を作成します。

WAF & Shield コンソールに遷移し、[IP Sets] - [Create IP Set] をクリックします。

IP Set の名前と説明、リージョンを入力します。
IP アドレスは末尾に「/32」をつけるなど CIDR フォーマットで入力してください。今回は 1 つしか入力しませんでしたが、複数 IP を登録する場合は 1 つの IP ごとに改行して入力してください。最後に「Create IP Set」をクリックします。

IP Sets が作成できました。

カスタムルールグループとカスタムルールの作成

続いてカスタムルールグループとカスタムルールを作成していきます。

[Rule groups] - [Create rule group] をクリックします。

ルールグループの名前と説明とリージョンを指定します。CloudWatch metric name は自動で入力されます。「Next」をクリックします。

「add rule」をクリックします。

今回はこのままビジュアルエディタで設定します。ルール名を入力し、タイプは「Regular rule」を選択します。

余談:「Rate-based rule」について(クリックで展開)

「Rate-based rule」は、条件に一致するリクエストについて、5 分間に許可されるリクエストの数を制限するというものです。最近この時間間隔は変更できるようになったそうです。

今回は以下のような複合ルールを設定しました。パス「/」宛てに、IP Sets に登録された IP からの通信が来た場合、

Block します、というルールです。

項目 備考
If a request matchs all the statements (AND)
Statement 1
Inspect URI path
Match type Exactly matches string
String to match /
Text transformation None
AND
Statement 2
Inspect Originates from an IP address in
IP set myIP 作成しておいた IP set
IP address to use as the originating address Source IP address
Then
Action Block

設定できたら「Add rule」をクリックします。

追加したルールの capacity は 3 になっていました。簡易なステートメントが一つだけだと capacity:1 になったりするのですが、今回は二つのステートメントを条件文でつなげたので 3 になったようです。

ルールグループの capacity は下段で設定します。ルールグループに含まれるルール全体の capacity より大きければ良いので、今回はバッファを持たせて 10 にしてみました。

ルールグループの capacity を設定する用途としては、複雑なルールを作り過ぎて capacity が Web ACL キャパシティーユニット(WCU)の許容量(1500 から有料、上限は 5000 まで)を超えないようにするためのもののようです。 設定できたら「Next」をクリックします。

次はルールの優先度(priority)を設定する画面です。ルールを複数設定した場合はここでアクセスを評価するルールの優先度を設定するのですが、今回は 1 つしかルールを設定しないので、このまま「Next」をクリックします。

最後に確認画面が表示されますので、設定値を確認して「Create rule group」をクリックします。

カスタムルールグループとカスタムルールが作成できました。

Web ACL の作成

最後に Web ACL を作成します。

AWS WAF を構築する ≒ WebACL を構築する、と思っていただいていいと思います。WebACLは AWS WAF における一番外枠のコンポーネントです。
「AWS WAF を ALB に紐づける ≒ WebACL を ALB に紐づける」です。

[Web ACLs] - [Create web ACL] をクリックします。

今回 ALB に WAF を紐づけるので、Resource type は Regional resources を選択します。CloudFront に紐づけたい場合は Amazon CloudFront distributions を選択してください。
リージョン、Web ACL の名前、説明を入力します。CloudWatch metric name は Web ACL の名前を入力すると自動で入力されます。

Associated AWS resources では「Add AWS resources」をクリックし、作成しておいた ALB を追加してください。ALB にチェックを付けたら「Next」をクリックします。

[Add rules] - [Add my own rules and rule groups] をクリックします。

Rule Type で Rule Group を選択すると、作成しておいたカスタムルールグループを設定できます。ここで設定するルールグループの名前は、Web ACL から見える名前になります。実際のルールグループの名前とは別のものを設定していただいても結構です。
今回はオーバーライド(Override rule group action)の設定はしません。「Add rule」をクリックします。

作成済みのカスタムルールグループを追加できました。

「Default web ACL action for requests that don't match any rules」では、どのルールにもマッチしなかった通信は最終的に何のアクションとするか設定します。
今回は Allow にして「Next」をクリックします。

ルールグループの優先度を設定します。今回は 1 つしかルールグループを設定しないので、このまま「Next」をクリックします。

Amazon CloudWatch metrics 使用すると、Web リクエスト、Web ACL、ルールを監視できるのでこのままチェックしておきます。

Request sampling options を有効にしておくと、Web ACL ルールに一致するリクエストを表示することができます。これも Enable sampling request にチェックしておきましょう。「Next」をクリックします。

確認画面です。設定値をチェックして、最後に「Create web ACL」をクリックします。

ログの有効化

Web ACL ができたらログを有効化します。作成した Web ACL の詳細を開き、「Logging and metrics」タブで Logging を確認します。今のままではログが無効になっているので、「Enable」をクリックして有効化しつつ、ログの設定をしていきます。

ログの保存場所を設定します。以下 3 種類の中からログの保存場所を選べます。

  • CloudWatch Logs
  • Amazon Data Firehose stream
  • S3 bucket

マネジメントコンソールからログをサクッと確認したいので、今回は CloudWatch Logs にします。ログを保存するロググループを作成するため、「Create new」をクリックします。

ブラウザの別タブで CloudWatch Logs の作成画面が開きます。WAF のログ保存用ロググループは「aws-waf-logs-」で始まる必要がありますので、名前を付ける際は注意してください。

ロググループ名、保持期間、ログクラスを設定したら、「作成」をクリックします。

ロググループができました。

WAF の画面に戻ります。ログ保存場所として作成したロググループを選択します。
ログを取得する際、count のみ、block のみなどのフィルタリングもできるのですが、今回は WAF を通過するすべての通信を取得してみましょう。Filter logs は何も設定せず、「Save」をクリックします。

ログの設定ができました。

CloudWatch Logs Insights からの確認

さて、WAF が設定できましたので、ちゃんとブロックさるか試してみましょう。IP Set に登録した IP アドレスから ALB の DNS 宛にブラウザから接続しようとすると、以下のように 403 Forbbiden となりました。しっかり WAF がブロックしてくれています。

何回かブラウザからアクセスを繰り返し、ログを溜めたら、CloudWatch Logs Insights からログを確認してみましょう。

CloudWatch コンソールで「ログのインサイト」をクリックします。

クエリは以下のように、action を含めてみました。時間を指定してクエリを実行すると、画像のようにアクセスがあった回数分ログが出力されており、それが Block なのか Allow なのかなどのアクションが分かります。

fields @timestamp, action, @message, @log
| sort @timestamp desc
| limit 1000

ログの詳細を開くとたくさんの情報が含まれています。

ご参考に 1 つ、Block のログ情報を以下に転記します。@message フィールドのみとても長いので、表の下部に別で貼り付けています。

フィールド
@ingestionTime 1710227339776
@log 123456789012:aws-waf-logs-emiki-test
@logStream ap-northeast-1_test-waf-webacl_22
@timestamp 1710227322507
action BLOCK
formatVersion 1
httpRequest.clientIp xx.xx.xx.xx
httpRequest.country JP
httpRequest.headers.0.name Accept-Language
httpRequest.headers.0.value ja,en;q=0.9,en-GB;q=0.8,en-US;q=0.7
httpRequest.headers.1.name Connection
httpRequest.headers.1.value keep-alive
httpRequest.headers.2.name Cache-Control
httpRequest.headers.2.value max-age=0
httpRequest.headers.3.name Upgrade-Insecure-Requests
httpRequest.headers.3.value 1
httpRequest.headers.4.name User-Agent
httpRequest.headers.4.value Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
httpRequest.headers.5.name Accept
httpRequest.headers.5.value text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
httpRequest.headers.6.name Accept-Encoding
httpRequest.headers.6.value gzip, deflate
httpRequest.headers.7.name Host
httpRequest.headers.7.value test-alb-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com
httpRequest.httpMethod GET
httpRequest.httpVersion HTTP/1.1
httpRequest.requestId 1-65efff7a-2520b1803dc0824d7c67a2b5
httpRequest.uri /
httpSourceId 123456789012-app/test-alb/93f4662cf108d565
httpSourceName ALB
ruleGroupList.0.ruleGroupId arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/test-rule-group/3bc5642d-7998-4f5c-9e89-xxxxxxxxxxxx
ruleGroupList.0.terminatingRule.action BLOCK
ruleGroupList.0.terminatingRule.ruleId test-rule
terminatingRuleId test-rule-group
terminatingRuleType GROUP
timestamp 1710227322507
webaclId arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/test-waf-webacl/69040105-54c5-4f80-b59c-xxxxxxxxxxxx
| @message | {"timestamp":1710227322507,"formatVersion":1,"webaclId":"arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/test-waf-webacl/69040105-54c5-4f80-b59c-xxxxxxxxxxxx","terminatingRuleId":"test-rule-group","terminatingRuleType":"GROUP","action":"BLOCK","terminatingRuleMatchDetails":\[\],"httpSourceName":"ALB","httpSourceId":"123456789012-app/test-alb/93f4662cf108d565","ruleGroupList":\[{"ruleGroupId":"arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/test-rule-group/3bc5642d-7998-4f5c-9e89-xxxxxxxxxxxx","terminatingRule":{"ruleId":"test-rule","action":"BLOCK","ruleMatchDetails":null},"nonTerminatingMatchingRules":\[\],"excludedRules":null,"customerConfig":null}\],"rateBasedRuleList":\[\],"nonTerminatingMatchingRules":\[\],"requestHeadersInserted":null,"responseCodeSent":null,"httpRequest":{"clientIp":"xx.xx.xx.xx","country":"JP","headers":\[{"name":"Accept-Language","value":"ja,en;q=0.9,en-GB;q=0.8,en-US;q=0.7"},{"name":"Connection","value":"keep-alive"},{"name":"Cache-Control","value":"max-age=0"},{"name":"Upgrade-Insecure-Requests","value":"1"},{"name":"User-Agent","value":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"},{"name":"Accept","value":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,\*/\*;q=0.8,application/signed-exchange;v=b3;q=0.7"},{"name":"Accept-Encoding","value":"gzip, deflate"},{"name":"Host","value":"test-alb-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com"}\],"uri":"/","args":"","httpVersion":"HTTP/1.1","httpMethod":"GET","requestId":"1-65efff7a-2520b1803dc0824d7c67a2b5"}} |

@logStream から該当のログストリームを表示すると、JSON 形式でログを表示することもできます。生の JSON ログも貼っておきます。

{
    "timestamp": 1710227322507,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:ap-northeast-1:123456789012:regional/webacl/test-waf-webacl/xxxxxxxxxx",
    "terminatingRuleId": "test-rule-group",
    "terminatingRuleType": "GROUP",
    "action": "BLOCK",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "ALB",
    "httpSourceId": "123456789012-app/test-alb/xxxxxxxxxx",
    "ruleGroupList": [
        {
            "ruleGroupId": "arn:aws:wafv2:ap-northeast-1:123456789012:regional/rulegroup/test-rule-group/xxxxxxxxxx",
            "terminatingRule": {
                "ruleId": "test-rule",
                "action": "BLOCK",
                "ruleMatchDetails": null
            },
            "nonTerminatingMatchingRules": [],
            "excludedRules": null,
            "customerConfig": null
        }
    ],
    "rateBasedRuleList": [],
    "nonTerminatingMatchingRules": [],
    "requestHeadersInserted": null,
    "responseCodeSent": null,
    "httpRequest": {
        "clientIp": "104.28.206.35",
        "country": "JP",
        "headers": [
            {
                "name": "Accept-Language",
                "value": "ja,en;q=0.9,en-GB;q=0.8,en-US;q=0.7"
            },
            {
                "name": "Connection",
                "value": "keep-alive"
            },
            {
                "name": "Cache-Control",
                "value": "max-age=0"
            },
            {
                "name": "Upgrade-Insecure-Requests",
                "value": "1"
            },
            {
                "name": "User-Agent",
                "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"
            },
            {
                "name": "Accept",
                "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
            },
            {
                "name": "Accept-Encoding",
                "value": "gzip, deflate"
            },
            {
                "name": "Host",
                "value": "test-alb-xxxxxxxxxx.ap-northeast-1.elb.amazonaws.com"
            }
        ],
        "uri": "/",
        "args": "",
        "httpVersion": "HTTP/1.1",
        "httpMethod": "GET",
        "requestId": "1-65efff7a-2520b1803dc0824d7c67a2b5"
    }
}

終わりに

AWS WAF を作成し、ログがどのような形式で出力されるか確認してみました。どなたかのお役に立てば幸いです。