AWS WAFのTop Insightsセクションの裏側で実行されるCloudWatch Logs Insightsの確認をしてみた
Top Insightsの裏側でどんなクエリを叩いているのか気になる
こんにちは、のんピ(@non____97)です。
皆さんはAWS WAFの運用をしていて、Top Insightセクションの裏側でどんなCloudWatch Insightsのクエリを叩いているのか気になるったことはありますか? 私はあります。
Top Insightsではアクセス回数が多かったURIパス100件や、送信元IPアドレスを簡単に確認することが可能です。
こちらのTop Insightsのダッシュボードの裏側ではCloudWatch Logsのログを参照しています。そして、CloudWatch Logs Insightsによってログのクエリが行われています。
実際にどんなクエリが叩かれていたのか気になったので確認してみます。
確認してみる
直近3時間の全ての通信
実際に試してみましょう。
直近3時間の全ての通信を確認してみます。
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回実行されていました。
実際のクエリは以下のとおりです。
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
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
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で確認します。
はい、結構ブロックされていますね。
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"
}
startTime
とendTime
および、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
の通信のみに絞って検索したいと言う場合に、指定された期間のログの全量がスキャンされてしまいそうです。
こちらの挙動の詳細は以下記事をご覧ください。
フィールドインデックス設定前どの程度スキャンするのか確認します。
以下記事の検証で使用した環境を再利用します。
こちらの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レコードがスキャンされたようです。
続いて、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レコードがスキャンされたようです。
誤ってログで出力設定をしていない終了アクションを選択した場合、コスト的にもったいないことになりそうですね。
フィールドインデックスの設定
フィールドインデックスの指定を行います。
今回はaction
を指定します。
ちなみにログの構造は以下のようになっています。
{
"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でクエリ実行をします。
結果は以下のとおりです。
ALLOW
を指定したものはスキャン件数が表示されずにこの時間範囲にはデータが見つかりません
と表示されるようになりました。
試しにクエリ実行範囲を5分から30分に変更します。この範囲にはフィールドインデックス設定前のアクセスが含まれます。
実行結果は以下のとおりです。
フィールドインデックスを利用
と表示されていることからフィールドインデックスが正常に使われていることが分かります。
CloudWatch Logs Insightsのフィールドインデックスは設定しておいても良いかも
AWS WAFのTop Insightsが裏側で実行するCloudWatch Logs Insightsの確認をしてみました。
action
程度しか設定する余地はないですが、CloudWatch Logs Insightsのフィールドインデックスは設定しておいても良さそうですね。特に追加料金はかからないですし、設定することによるデメリットは特にないように思えます。
この記事が誰かの助けになれば幸いです。
以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!