AWS WAFでカウントされたトラフィックをフィールドインデックスを用いて効率良くCloudWatch Logs Insightsでクエリしてみたかった
カウントされたトラフィックを抽出したい
こんにちは、のんピ(@non____97)です。
皆さんはAWS WAFの運用をしていて、カウントされたトラフィックを抽出したいなと思ったことはありますか? 私はあります。
新規にルールを追加する際は、いきなりブロックするのではなく、まずはカウントモードとして様子を見ることがほとんどだと思います。
では、カウントモード中に実際に検出されたトラフィックはどのように確認すれば良いでしょうか。
CloudWatch Logsに出力している場合はCloudWatch Logs Insights、S3場合に出力している場合はAthenaによるクエリが思い付きます。
ただ、最近AWS WAFのTraffic overviewダッシュボードに追加されたTop Insightsが便利すぎて、CloudWatch Logsに出力している場合も多いのではないでしょうか。
ということで、こちらの記事ではCloudWatch Logs Insightsを用いて、カウントされたトラフィックを抽出してみます。
いきなりまとめ
- フィールドインデックスを用いて、AWS WAFのルールでカウントされたトラフィックを検出することは難しい
- 現実的にはフィールドインデックスの効かない
like
を用いて検索することになりそう
どのような処理が必要かの検討
まず、どのような処理が必要かの検討をしてみましょう。
ログの配信にALLOW
を含まないようにしている場合は、ログオブジェクト直下が"action" : "allow"
であることを条件に、全てのルールでブロックされなかったトラフィックを確認することが可能です。
ただし、以下のような場合にはこれだけでは検出することができません。
- ログの配信対象に
ALLOW
も含んでいる場合 - 最終的にブロックやCAPTCHA、Challengeされたトラフィックであっても途中のルールでカウントされたものを抽出したい場合
1つ目については「"action" : "count"
で検出できるのでは?」と、つい思われがちですが、出来ません。ログオブジェクト直下のaction
フィールドにはcount
は含まれないためです。
action
The terminating action that AWS WAF applied to the request. This indicates either allow, block, CAPTCHA, or challenge. The CAPTCHA and Challenge actions are terminating when the web request doesn't contain a valid token.
Log fields for web ACL traffic - AWS WAF, AWS Firewall Manager, and AWS Shield Advanced
2つ目については、COUNT
やEXCLUDED_AS_COUNT
が非終了アクションではないことが関係しています。
COUNT
やEXCLUDED_AS_COUNT
と判定されたルールは、そのタイミングで以降のルール評価を行わないのではなく、優先度に基づいて継続してルールの評価を行います。
つまりは、対象の全ルールの評価をする過程でCOUNT
やEXCLUDED_AS_COUNT
と判定されたとしても、最終的に許可されるのかブロックされるのかは後続のルールもしくはデフォルトアクション次第です。場合によってはブロックされることもあるでしょう。
結果として、"action" : "allow"
のみで抽出する場合、最終的に許可されたものしか抽出することができません。
そのため、「ログオブジェクト直下が"action" : "allow"
であることを条件に」だけではなく、別の手法で抽出する必要があります。
カウントされたトラフィックは以下のように記録されています。
- WAF ログ直下の "nonTerminatingMatchingRules" フィールドに
"action": "COUNT"
として記録 - "ruleGroupList" フィールド内の "nonTerminatingMatchingRules" フィールドに
"action": "COUNT"
として記録 - "ruleGroupList" フィールド内の "excludedRules" フィールドに
"exclusionType": "EXCLUDED_AS_COUNT"
として記録
詳細は以下記事をご覧ください。
AWS公式ブログではCloudWatch Logs Insightsを使用する場合、以下のようなクエリで抽出をしていました。
fields @timestamp, @message
| filter @message like "EXCLUDED_AS_COUNT"
| sort @timestamp desc
| parse @message '"excludedRules":[{*}]' as excludedRules
| display @timestamp, httpRequest.clientIp, httpRequest.uri, httpRequest.country, excludedRules
fields @timestamp, @message
| filter @message like /"action":"COUNT"/
| sort @timestamp desc
| parse @message '"nonTerminatingMatchingRules":[{*}]' as nonTerminatingMatchingRules
| display @timestamp, httpRequest.clientIp, httpRequest.uri, httpRequest.country, nonTerminatingMatchingRules
しかし、こちらの手法ですとCloudWatch Logsのフィールドインデックスの使用ができません。
フィールドインデックスを使用した場合はスキャン量を減らすことができ、コストおよび実行時間を削減することが可能です。
しかし、上述のクエリ例ではフィールドインデックスをサポートしないlike
が使用されています。
フィールドインデックスの改善の恩恵を受けるの
filter fieldName IN...
は、filter fieldName =...
と を持つクエリのみです。を使用したクエリではインデックスは使用filter fieldName like
されず、選択したロググループのすべてのログイベントを常にスキャンします。
そのため、フィールドインデックスを有効活用したい場合においては、こちらのクエリをそのまま採用することはできません。
ALLOW
も出力しているなどログ配信量が多い場合に、フィールドインデックスが有効活用されない場合CloudWatch Logs Insightsを実行する度にかかるコストが気になります。
ということで、それらを意識しながらクエリを組み立てる必要があります。
CAPTCHA
とChallenge
を使用していない場合
ルールグループの個別ルール単位でCOUNTをしていない かつ 先に結論から紹介すると、全ての要件を満たすクエリを作り出すことはできませんでした。
要件を整理すると以下が挙げられます。
- フィールドインデックスを用いたコスト削減ができること
- ログの配信対象に
ALLOW
やCAPTCHA
、Challenge
も含んでいる場合もCOUNT
もしくはEXCLUDED_AS_COUNT
のみ抽出できること - カスタムルールのアクションを
COUNT
に設定したルールの検出ができること - ルールグループのアクションが
COUNT
にオーバーライドされたルールグループの検出ができること - ルールグループ内、個別のルールのデフォルトアクションを
COUNT
に設定したルールの検出ができること - ルールグループ内、個別のルールのアクションを
COUNT
にオーバライドされたルールの検出ができること - ルールグループ内、個別のルールのアクションを
ExcludedRules
を用いて、COUNT
としたルールの検出ができること
「手軽に」「コストを極力抑えるために」という意味合いで要件1の「フィールドインデックスを用いたコスト削減ができること」を満たそうとすると、どうしても実現できない要件があります。
例えば以下のクエリは以下の要件のみ満たします。
「1. フィールドインデックスを用いたコスト削減ができること」
「3. カスタムルールのアクションをCOUNT
に設定したルールの検出ができること」
「4. ルールグループのアクションがCOUNT
にオーバーライドされたルールグループの検出ができること」
fields @timestamp,
@message
| filterIndex nonTerminatingMatchingRules.0.action="COUNT"
| sort @timestamp desc
| parse @message '"nonTerminatingMatchingRules":[{*}]' as nonTerminatingMatchingRulesObject
| limit 100
| display @timestamp,
httpRequest.clientIp,
httpRequest.uri,
httpRequest.httpMethod,
httpRequest.country,
action,
terminatingRuleId,
httpRequest.requestId,
nonTerminatingMatchingRulesObject
ルールグループの個別ルール単位でCOUNT
をしていない かつ CAPTCHA
とChallenge
を使用していない場合にのみ使用できます。以下の要件を満たすことが必要な場合は使用できません。
「2. ログの配信対象にALLOW
やCAPTCHA
、Challenge
も含んでいる場合もCOUNT
もしくはEXCLUDED_AS_COUNT
のみ抽出できること」
「5. ルールグループ内、個別のルールのデフォルトアクションをCOUNT
に設定したルールの検出ができること」
「6. ルールグループ内、個別のルールのアクションをCOUNT
にオーバライドされたルールの検出ができること」
「7. ルールグループ内、個別のルールのアクションをExcludedRules
を用いて、COUNT
としたルールの検出ができること」
まず、「2. ログの配信対象にALLOW
やCAPTCHA
、Challenge
も含んでいる場合もCOUNT
もしくはEXCLUDED_AS_COUNT
のみ抽出できること」という要件についてです。
これは、filterIndex nonTerminatingMatchingRules.0.action="COUNT"
と関係があります。
nonTerminatingMatchingRules
は非終了ルールの配列です。また、action
はcount
以外も存在します。
nonTerminatingMatchingRules
The list of non-terminating rules that matched the request. Each item in the list contains the following information.
action
The action that AWS WAF applied to the request. This indicates either count, CAPTCHA, or challenge. The CAPTCHA and Challenge are non-terminating when the web request contains a valid token.ruleId
The ID of the rule that matched the request and was non-terminating.ruleMatchDetails
Detailed information about the rule that matched the request. This field is only populated for SQL injection and cross-site scripting (XSS) match rule statements. A matching rule might require a match for more than one inspection criteria, so these match details are provided as an array of match criteria.Any additional information provided for each rule varies according factors such as the rule configuration, rule match type, and details of the match. For example for rules with a CAPTCHA or Challenge action, the captchaResponse or challengeResponse will be listed. If the matching rule is in a rule group and you've overridden its configured rule action, the configured action will be provided in overriddenAction.
Log fields for web ACL traffic - AWS WAF, AWS Firewall Manager, and AWS Shield Advanced
そのため、filterIndex nonTerminatingMatchingRules.0.action="COUNT"
とした場合、COUNT
以外のトラフィックを検出することができません。
また、配列の0番目を見ているため、0番目の要素がCAPTCHA
やChallenge
などCOUNT
以外のactionが取られた場合もトラフィックを検出することができません。
「filterIndex nonTerminatingMatchingRules.*.action="COUNT"
やfilterIndex nonTerminatingMatchingRules.[*].action="COUNT"
といった指定をすればできるのでは?」と思われるかも知れませんが、2025/3/17時点でCloudWatch Logs Insightsでそのような構文はサポートされていません。
つまりは配列内のフィールドとフィールドインデックスとの相性は良くありません。
結果として、CAPTCHA
やChallenge
を使用している場合はこのクエリを使用することはできません。
「5. ルールグループ内、個別のルールのデフォルトアクションをCOUNT
に設定したルールの検出ができること」
「6. ルールグループ内、個別のルールのアクションをCOUNT
にオーバライドされたルールの検出ができること」
「7. ルールグループ内、個別のルールのアクションをExcludedRules
を用いて、COUNT
としたルールの検出ができること」
については、COUNT
もしくはEXCLUDED_AS_COUNT
として判定されたルールは、ruleGroupList
の配列の中のnonTerminatingMatchingRules
およびexcludedRules
にあるというのがポイントです。
ruleGroupList.*.nonTerminatingMatchingRules
配下にあることを確認してみましょう。
AWSManagedRulesCommonRuleSet
のNoUserAgent_HEADER
とGenericLFI_URIPATH
、RestrictedExtensions_QUERYARGUMENTS
、AWSManagedRulesKnownBadInputsRuleSet
のExploitablePaths_URIPATH
をCOUNT
にオーバーライドしました。
この状態で以下のようにNoUserAgent_HEADER
とRestrictedExtensions_QUERYARGUMENTS
、ExploitablePaths_URIPATH
がマッチするようにアクセスします。
> curl -I https://www.non-97.net/WEB-INF/web.xml?id=test.ini -H "User-Agent:"
HTTP/2 404
content-type: text/html
content-length: 12
date: Mon, 17 Mar 2025 00:17:38 GMT
last-modified: Tue, 25 Feb 2025 02:38:39 GMT
etag: "347dfa37997b9353b3da6992f8753439"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 792d1dfcd0e864258cddb08b00eca5d8.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: M1CG8alBGw4Bbqk1bhQWCImSGRyu9nt8bjfz1NcralRm28HLpfxI4g==
age: 17952
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
404ということで、拒否はされていないようです。
WAFのログを見ます。
{
"timestamp": 1742188608710,
"formatVersion": 1,
"webaclId": "arn:aws:wafv2:us-east-1:<AWSアカウントID>:global/webacl/website/84a704a7-6965-44af-887e-d196dcd881ac",
"terminatingRuleId": "Default_Action",
"terminatingRuleType": "REGULAR",
"action": "ALLOW",
"terminatingRuleMatchDetails": [],
"httpSourceName": "CF",
"httpSourceId": "ELGQOVCCUO3ME",
"ruleGroupList": [
{
"ruleGroupId": "AWS#AWSManagedRulesCommonRuleSet",
"terminatingRule": null,
"nonTerminatingMatchingRules": [
{
"ruleId": "NoUserAgent_HEADER",
"action": "COUNT",
"overriddenAction": "BLOCK",
"ruleMatchDetails": []
},
{
"ruleId": "RestrictedExtensions_QUERYARGUMENTS",
"action": "COUNT",
"overriddenAction": "BLOCK",
"ruleMatchDetails": [
{
"conditionType": "REGEX",
"location": "ALL_QUERY_ARGS",
"matchedData": null,
"matchedFieldName": ""
}
]
}
],
"excludedRules": null,
"customerConfig": null
},
{
"ruleGroupId": "AWS#AWSManagedRulesKnownBadInputsRuleSet",
"terminatingRule": null,
"nonTerminatingMatchingRules": [
{
"ruleId": "ExploitablePaths_URIPATH",
"action": "COUNT",
"overriddenAction": "BLOCK",
"ruleMatchDetails": [
{
"conditionType": "REGEX",
"location": "URI",
"matchedData": null,
"matchedFieldName": ""
}
]
}
],
"excludedRules": null,
"customerConfig": null
},
{
"ruleGroupId": "AWS#AWSManagedRulesAmazonIpReputationList",
"terminatingRule": null,
"nonTerminatingMatchingRules": [],
"excludedRules": null,
"customerConfig": null
},
{
"ruleGroupId": "AWS#AWSManagedRulesAnonymousIpList",
"terminatingRule": null,
"nonTerminatingMatchingRules": [],
"excludedRules": null,
"customerConfig": null
}
],
"rateBasedRuleList": [],
"nonTerminatingMatchingRules": [],
"requestHeadersInserted": null,
"responseCodeSent": null,
"httpRequest": {
"clientIp": "<送信元IPアドレス>",
"country": "JP",
"headers": [
{
"name": "host",
"value": "www.non-97.net"
},
{
"name": "accept",
"value": "*/*"
}
],
"uri": "/WEB-INF/web.xml",
"args": "id=test.ini",
"httpVersion": "HTTP/2.0",
"httpMethod": "HEAD",
"requestId": "M1CG8alBGw4Bbqk1bhQWCImSGRyu9nt8bjfz1NcralRm28HLpfxI4g==",
"fragment": "",
"scheme": "https",
"host": "www.non-97.net"
},
"labels": [
{
"name": "awswaf:managed:aws:core-rule-set:NoUserAgent_Header"
},
{
"name": "awswaf:managed:aws:known-bad-inputs:ExploitablePaths_URIPath"
},
{
"name": "awswaf:managed:aws:core-rule-set:RestrictedExtensions_QueryArguments"
}
],
"ja3Fingerprint": "5ba6f86deff79afc9902f9927d1c1697",
"ja4Fingerprint": "t13d3013h2_1d37bd780c83_e10b9050f4c9"
}
ruleGroupList
内のnonTerminatingMatchingRules
で検出されていることが分かります。
先述のとおり、配列内のフィールドとフィールドインデックスとの相性は良くないため、ruleGroupList.*.nonTerminatingMatchingRules.*.action="COUNT"
といったことはできません。
こちらのクエリで実際にフィールドインデックスが効くことを確認してみましょう。
フィールドインデックスはnonTerminatingMatchingRules.0.action
を指定しました。
AWSManagedRulesCommonRuleSet
とAWSManagedRulesKnownBadInputsRuleSet
でOverride rule group action to count
を有効にします。
個別ルールのオーバーライドは削除しておきます。
この状態で、COUNT
非対象のリクエストを5回、COUNT
対象のリクエストを5回実行します。
> curl -I https://www.non-97.net/ -s >/dev/null
> curl -I https://www.non-97.net/ -s >/dev/null
> curl -I https://www.non-97.net/ -s >/dev/null
> curl -I https://www.non-97.net/ -s >/dev/null
> curl -I https://www.non-97.net/ -s >/dev/null
> curl -I https://www.non-97.net/WEB-INF/web.xml?id=test.ini -H "User-Agent:" -s >/dev/null
> curl -I https://www.non-97.net/WEB-INF/web.xml?id=test.ini -H "User-Agent:" -s >/dev/null
> curl -I https://www.non-97.net/WEB-INF/web.xml?id=test.ini -H "User-Agent:" -s >/dev/null
> curl -I https://www.non-97.net/WEB-INF/web.xml?id=test.ini -H "User-Agent:" -s >/dev/null
> curl -I https://www.non-97.net/WEB-INF/web.xml?id=test.ini -H "User-Agent:" -s >/dev/null
実行結果は以下のとおりです。
フィールドインデックスを利用
と表示されていることから、フィールドインデックスに基づいてクエリされていることが分かります。
なお、スキャンされたのが5レコードではなく、6レコードなのは謎です。何回か試しましたが、いずれも本来のレコード数+1となっていました。
他にもisempty()
やispresent()
など関数を用いてフィールドを加工した場合や否定を使ったクエリも考えましたが、いずれもインデックスは効きません。当然といえば当然です。RDBと同じですね。
AWS WAFでカウントされたトラフィックをフィールドインデックスを用いて効率良くCloudWatch Logs Insightsでクエリするのは難易度が高い
AWS WAFでカウントされたトラフィックをフィールドインデックスを用いて効率良くCloudWatch Logs Insightsでクエリしようとしてみました。
結論、難易度が高いです。
カウントされたルールの数などの専用のフィールドがなければ、フィールドインデックスを用いてカウントされたトラフィックのみを抽出するのは難しいと考えます。
現実的にはフィールドインデックスの効かないlike
を用いて検索することになりそうです。
この記事が誰かの助けになれば幸いです。
以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!