AWS WAFのTop Insightsセクションの裏側で実行されるCloudWatch Logs Insightsの確認をしてみた

AWS WAFのTop Insightsセクションの裏側で実行されるCloudWatch Logs Insightsの確認をしてみた

CloudWatch Logs Insightsのフィールドインデックスは設定しておいても良いかも
Clock Icon2025.03.04

Top Insightsの裏側でどんなクエリを叩いているのか気になる

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

皆さんはAWS WAFの運用をしていて、Top Insightセクションの裏側でどんなCloudWatch Insightsのクエリを叩いているのか気になるったことはありますか? 私はあります。

Top Insightsではアクセス回数が多かったURIパス100件や、送信元IPアドレスを簡単に確認することが可能です。

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

こちらのTop Insightsのダッシュボードの裏側ではCloudWatch Logsのログを参照しています。そして、CloudWatch Logs Insightsによってログのクエリが行われています。

実際にどんなクエリが叩かれていたのか気になったので確認してみます。

確認してみる

直近3時間の全ての通信

実際に試してみましょう。

直近3時間の全ての通信を確認してみます。

1.Top Insights1.png

CloudTrailにて、この時のStartQueryイベントを確認します。

{
    "eventVersion": "1.11",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "<プリンシパルID>:<IAMユーザー名>",
        "arn": "arn:aws:sts::<AWSアカウントID>:assumed-role/<IAMロール名>/<IAMユーザー名>",
        "accountId": "<AWSアカウントID>",
        "accessKeyId": "<アクセスキーID>>",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "<プリンシパルID>",
                "arn": "arn:aws:iam::<AWSアカウントID>:role/<IAMロール名>",
                "accountId": "<AWSアカウントID>",
                "userName": "<IAMユーザー名>"
            },
            "attributes": {
                "creationDate": "2025-03-04T00:55:28Z",
                "mfaAuthenticated": "true"
            }
        }
    },
    "eventTime": "2025-03-04T00:55:55Z",
    "eventSource": "logs.amazonaws.com",
    "eventName": "StartQuery",
    "awsRegion": "us-east-1",
    "sourceIPAddress": "<送信元IPアドレス>",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
    "requestParameters": {
        "queryLanguage": "CWLI",
        "logGroupName": "aws-waf-logs-website",
        "startTime": 1741038954328,
        "endTime": 1741049754328,
        "queryString": "fields httpRequest.uri as uri, action\n                        | filter action in ['BLOCK', 'ALLOW', 'CAPTCHA', 'CHALLENGE'] | filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac']\n                        \n                        | stats count(*) as cnt by uri \n                        | sort cnt desc \n                        | limit 100",
        "limit": 100,
        "dryRun": false
    },
    "responseElements": {
        "queryId": "93425d40-5fe5-47b1-8f27-8037a11eea9b"
    },
    "requestID": "e2032e33-5586-4920-b06c-c2a433d13c8f",
    "eventID": "f5ac8caa-f394-42f1-ae93-79c6417c9798",
    "readOnly": false,
    "resources": [
        {
            "accountId": "<AWSアカウントID>",
            "type": "AWS::Logs::LogGroup",
            "ARN": "arn:aws:logs:us-east-1:<AWSアカウントID>:log-group:aws-waf-logs-website"
        }
    ],
    "eventType": "AwsApiCall",
    "apiVersion": "20140328",
    "managementEvent": true,
    "recipientAccountId": "<AWSアカウントID>",
    "eventCategory": "Management",
    "tlsDetails": {
        "tlsVersion": "TLSv1.3",
        "cipherSuite": "TLS_AES_128_GCM_SHA256",
        "clientProvidedHostHeader": "logs.us-east-1.amazonaws.com"
    },
    "sessionCredentialFromConsole": "true"
}

queryStringに実行しているクエリが記載されていますね。

整形すると、以下のような形です。

fields httpRequest.uri as uri, action
| filter action in ['BLOCK', 'ALLOW', 'CAPTCHA', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac']
| stats count(*) as cnt by uri 
| sort cnt desc 
| limit 100

URIパスごとの検知数をカウントしていることが分かります。パネルで言うとTop 100 URI paths ranked by request frequency.です。

また、サービスリンクロールではなく、ユーザーの権限で実行されているようです。

同様なものがクエリを変更してTop Insightsのパネル数分 = 6回実行されていました。

実際のクエリは以下のとおりです。

クライアントIPアドレスごとの検知数
fields httpRequest.clientIp, action
| filter action in ['BLOCK', 'ALLOW', 'CAPTCHA', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac']
| stats count(*) as cnt by httpRequest.clientIp 
| sort cnt desc 
| limit 100
ラベルごとの検知数
fields  jsonParse(@message) as json_message , action
| filter action in ['BLOCK', 'ALLOW', 'CAPTCHA', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac']
| unnest json_message.labels into labelsvalues
| stats count(*) as cnt by labelsvalues.name
| sort cnt desc 
| limit 100
WebACLが関連づけられているリソース毎の検知数
stats count(*) as cnt by concat(httpSourceName, ' | ', httpSourceId) as combined_field 
| filter action in ['BLOCK', 'ALLOW', 'CAPTCHA', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac'] 
| sort cnt desc 
| limit 100
User Agentごとの検知数
fields @timestamp, @message 
| parse @message /\\{\"name\":\"(U|u)ser-(A|a)gent\",\"value\":\"(?<userAgent>.*?)\"\\}/ 
| filter action in ['BLOCK', 'ALLOW', 'CAPTCHA', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac'] 
| stats count(*) as requestCount by userAgent 
| sort requestCount desc 
| limit 100
リクエストメソッドごとの検知数
fields httpRequest.httpMethod, action 
| filter action in ['BLOCK', 'ALLOW', 'CAPTCHA', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac'] 
| stats count(*) as count by httpRequest.httpMethod 
| sort count desc 
| limit 100

直近一週間のBLOCKとCHALENGEの通信

直近一週間のBLOCKとCHALENGEの通信もTop Insightsで確認します。

3.Top Insights3.png

はい、結構ブロックされていますね。

CloudTrailにて、この時のStartQueryイベントを確認します。

{
    "eventVersion": "1.11",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "<プリンシパルID>:<IAMユーザー名>",
        "arn": "arn:aws:sts::<AWSアカウントID>:assumed-role/<IAMロール名>/<IAMユーザー名>",
        "accountId": "<AWSアカウントID>",
        "accessKeyId": "<アクセスキーID>>",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "<プリンシパルID>",
                "arn": "arn:aws:iam::<AWSアカウントID>:role/<IAMロール名>",
                "accountId": "<AWSアカウントID>",
                "userName": "<IAMユーザー名>"
            },
            "attributes": {
                "creationDate": "2025-03-04T00:55:28Z",
                "mfaAuthenticated": "true"
            }
        }
    },
    "eventTime": "2025-03-04T02:11:28Z",
    "eventSource": "logs.amazonaws.com",
    "eventName": "StartQuery",
    "awsRegion": "us-east-1",
    "sourceIPAddress": "<送信元IPアドレス>",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
    "requestParameters": {
        "queryLanguage": "CWLI",
        "logGroupName": "aws-waf-logs-website",
        "startTime": 1740449484146,
        "endTime": 1741054284146,
        "queryString": "fields httpRequest.httpMethod, action | filter action in ['BLOCK', 'CHALLENGE'] | filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac'] | stats count(*) as count by httpRequest.httpMethod | sort count desc | limit 100",
        "limit": 100,
        "dryRun": false
    },
    "responseElements": {
        "queryId": "a6fd7c76-05e4-443e-95f1-b4c3bcfe2f40"
    },
    "requestID": "a1433a41-b641-4e91-bdbc-aa528744644a",
    "eventID": "f9afc28d-14e6-48e0-adf5-59cc61ec680b",
    "readOnly": false,
    "resources": [
        {
            "accountId": "<AWSアカウントID>",
            "type": "AWS::Logs::LogGroup",
            "ARN": "arn:aws:logs:us-east-1:<AWSアカウントID>:log-group:aws-waf-logs-website"
        }
    ],
    "eventType": "AwsApiCall",
    "apiVersion": "20140328",
    "managementEvent": true,
    "recipientAccountId": "<AWSアカウントID>",
    "eventCategory": "Management",
    "tlsDetails": {
        "tlsVersion": "TLSv1.3",
        "cipherSuite": "TLS_AES_128_GCM_SHA256",
        "clientProvidedHostHeader": "logs.us-east-1.amazonaws.com"
    },
    "sessionCredentialFromConsole": "true"
}

startTimeendTimeおよび、filter action inのクエリが変わった以外は差はなさそうです。

整形すると以下のとおりです。

fields httpRequest.httpMethod, action 
| filter action in ['BLOCK', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac'] 
| stats count(*) as count by httpRequest.httpMethod 
| sort count desc 
| limit 100

他のイベントも以下のとおり、クエリ自体はfilter action inの箇所のみ変わっているようでした。

fields httpRequest.uri as uri, action
| filter action in ['BLOCK', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac']
| stats count(*) as cnt by uri 
| sort cnt desc 
| limit 100
fields @timestamp, @message 
| parse @message /\\{\"name\":\"(U|u)ser-(A|a)gent\",\"value\":\"(?<userAgent>.*?)\"\\}/ | filter action in ['BLOCK', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac'] 
| stats count(*) as requestCount by userAgent 
| sort requestCount desc 
| limit 100
stats count(*) as cnt by concat(httpSourceName, ' | ', httpSourceId) as combined_field 
| filter action in ['BLOCK', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac'] 
| sort cnt desc 
| limit 100
fields jsonParse(@message) as json_message , action
| filter action in ['BLOCK', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac']
| unnest json_message.labels into labelsvalues
| stats count(*) as cnt by labelsvalues.name
| sort cnt desc 
| limit 100
fields httpRequest.clientIp, action
| filter action in ['BLOCK', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac']
| stats count(*) as cnt by httpRequest.clientIp 
| sort cnt desc 
| limit 100

CloudWatch Logs Insightsのフィールドインデックスの設定

設定前のクエリ実行

せっかくなので、CloudWatch Logs Insightsフィールドインデックスを設定してみましょう。

このままだと、たとえばALLOWの通信のみに絞って検索したいと言う場合に、指定された期間のログの全量がスキャンされてしまいそうです。

こちらの挙動の詳細は以下記事をご覧ください。

https://dev.classmethod.jp/articles/cwlogs-insights-query-scan-volume-not-affect-costs/

https://dev.classmethod.jp/articles/cwlogs-support-field-index/

フィールドインデックス設定前どの程度スキャンするのか確認します。

以下記事の検証で使用した環境を再利用します。

https://dev.classmethod.jp/articles/aws-cdk-waf-rate-based-rule-block-same-ip-uri-path-access/

こちらのAWS WAFでは1分間に10回以上同一のIPアドレスから同一のURIパスにアクセスした際にブロックするようなレートベースルールを設定しています。

以下のように1秒間隔でアクセスして、ブロックされることを確認します。

$ 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 26: 200
Request 27: 200
Request 28: 200
Request 29: 403
Request 30: 403
Request 31: 403
Request 32: 403
.
.
(中略)
.
.
Request 98: 403
Request 99: 403
Request 100: 403

この状態でCloudWatch Logs Insightsでクエリをかけます。

実行するクエリは以下のとおりです。

fields httpRequest.clientIp, action
| filter action in ['BLOCK', 'ALLOW', 'CAPTCHA', 'CHALLENGE'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac']
| stats count(*) as cnt by httpRequest.clientIp 
| sort cnt desc 
| limit 100

実行結果を確認すると59レコードがスキャンされたようです。

8_フィールドインデックス指定前_ALL.png

続いて、ALLOWのみに絞って実行します。

fields httpRequest.clientIp, action
| filter action in ['ALLOW'] 
| filter webaclId in ['arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac']
| stats count(*) as cnt by httpRequest.clientIp 
| sort cnt desc 
| limit 100

WebACLではブロックとカウントの2種類しか出力させていないため、そもそもALLOWはログに含まれません。

実行結果を確認すると、72レコードがスキャンされたようです。

9_フィールドインデックス指定前_Allow.png

誤ってログで出力設定をしていない終了アクションを選択した場合、コスト的にもったいないことになりそうですね。

フィールドインデックスの設定

フィールドインデックスの指定を行います。

今回はactionを指定します。

6.フィールドインデックスの指定.png

7.フィールドインデックスの指定2.png

ちなみにログの構造は以下のようになっています。

{
    "timestamp": 1741061666998,
    "formatVersion": 1,
    "webaclId": "arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac",
    "terminatingRuleId": "AWSManagedRulesAnonymousIpList",
    "terminatingRuleType": "MANAGED_RULE_GROUP",
    "action": "BLOCK",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "CF",
    "httpSourceId": "ELGQOVCCUO3ME",
    "ruleGroupList": [
        {
            "ruleGroupId": "AWS#AWSManagedRulesCommonRuleSet",
            "terminatingRule": null,
            "nonTerminatingMatchingRules": [],
            "excludedRules": null,
            "customerConfig": null
        },
        {
            "ruleGroupId": "AWS#AWSManagedRulesKnownBadInputsRuleSet",
            "terminatingRule": null,
            "nonTerminatingMatchingRules": [],
            "excludedRules": null,
            "customerConfig": null
        },
        {
            "ruleGroupId": "AWS#AWSManagedRulesAmazonIpReputationList",
            "terminatingRule": null,
            "nonTerminatingMatchingRules": [],
            "excludedRules": null,
            "customerConfig": null
        },
        {
            "ruleGroupId": "AWS#AWSManagedRulesAnonymousIpList",
            "terminatingRule": {
                "ruleId": "HostingProviderIPList",
                "action": "BLOCK",
                "ruleMatchDetails": null
            },
            "nonTerminatingMatchingRules": [],
            "excludedRules": null,
            "customerConfig": null
        }
    ],
    "rateBasedRuleList": [
        {
            "rateBasedRuleId": "wafv2:us-east-1:<AWSアカウントID>:global/ARBR/84a704a7-6965-44af-887e-d196dcd881ac_ARBR/60ab1100c37b976e14f64eba661d5c9c_RateLimit_SameIPSameURI",
            "rateBasedRuleName": "RateLimit_SameIPSameURI",
            "limitKey": "CUSTOMKEYS",
            "maxRateAllowed": 10,
            "evaluationWindowSec": 60,
            "customValues": [
                {
                    "key": "URI",
                    "name": "",
                    "value": "/"
                },
                {
                    "key": "IP",
                    "name": "",
                    "value": "<送信元IPアドレス>"
                }
            ]
        }
    ],
    "nonTerminatingMatchingRules": [],
    "requestHeadersInserted": null,
    "responseCodeSent": null,
    "httpRequest": {
        "clientIp": "<送信元IPアドレス>",
        "country": "SG",
        "headers": [
            {
                "name": "Host",
                "value": "www.non-97.net"
            },
            {
                "name": "User-Agent",
                "value": "<User Agent>"
            },
            {
                "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"
            },
            {
                "name": "Accept-Language",
                "value": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"
            },
            {
                "name": "Cache-Control",
                "value": "no-cache"
            },
            {
                "name": "Connection",
                "value": "keep-alive"
            },
            {
                "name": "Pragma",
                "value": "no-cache"
            },
            {
                "name": "Referer",
                "value": "http://www.non-97.net"
            },
            {
                "name": "Upgrade-Insecure-Requests",
                "value": "1"
            },
            {
                "name": "Connection",
                "value": "close"
            }
        ],
        "uri": "/",
        "args": "",
        "httpVersion": "HTTP/1.1",
        "httpMethod": "GET",
        "requestId": "B5KPMcgyUdqPHwbGFL7f9g6fr1SNHnVNbo_yJ7m2l34a2COr_XXzCQ==",
        "fragment": "",
        "scheme": "https",
        "host": "www.non-97.net"
    },
    "labels": [
        {
            "name": "awswaf:managed:aws:anonymous-ip-list:HostingProviderIPList"
        }
    ],
    "ja3Fingerprint": "473cd7cb9faa642487833865d516e578",
    "ja4Fingerprint": "t13d190900_9dc949149365_97f8aa674fd9"
}

フィールドインデックス設定後のクエリ実行

フィールドインデックス設定後に再度クエリを実行します。

フィールドインデックス設定前のログにはインデックスは適用されないため、先ほどと同様に1秒間隔で同じURIパスにアクセスし続けます。

その後、CloudWatch Logs Insightsでクエリ実行をします。

結果は以下のとおりです。

10.フィールドインデックス指定後.png

11.スキャン量が表示されなくなる.png

ALLOWを指定したものはスキャン件数が表示されずにこの時間範囲にはデータが見つかりませんと表示されるようになりました。

試しにクエリ実行範囲を5分から30分に変更します。この範囲にはフィールドインデックス設定前のアクセスが含まれます。

実行結果は以下のとおりです。

12_時間を30分に変更.png

フィールドインデックスを利用と表示されていることからフィールドインデックスが正常に使われていることが分かります。

CloudWatch Logs Insightsのフィールドインデックスは設定しておいても良いかも

AWS WAFのTop Insightsが裏側で実行するCloudWatch Logs Insightsの確認をしてみました。

action程度しか設定する余地はないですが、CloudWatch Logs Insightsのフィールドインデックスは設定しておいても良さそうですね。特に追加料金はかからないですし、設定することによるデメリットは特にないように思えます。

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

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

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.