AWS WAF 便利ですね。最近は AWS Managed Rules が導入されて簡単に WAF ルールを適用することができるようになりました。ただ、独自でルールを作るのに比較してブラックボックス化されやすいため本番環境に導入する前に、どういったリクエストがルールにヒットするのかを調査した上で導入したくなります。

というわけで、AWS WAF のリクエストログを S3 に保存して調査する方法をメモしておきます。

AWS WAF のログを Firehose から S3 に保存する

基本はドキュメント通りに設定すれば完了するはずです。

Firehose の作成

  • Kinesis Firehose 配信ストリームの作成を選択
  • Delivery stream name に aws-waf-logs- で始まる適当な名前を入力します。これは必ずこの prefix で始まる必要があります。それ以外はデフォルトのまま次へ
  • Transform source records と Convert record format もデフォルトのまま次へ
  • S3 destination で S3 バケットを作成します、ここの名前は適当で大丈夫です。それ以外はデフォルトのまま次へ
  • 以降は全てデフォルトのままで問題ないです

WAF の設定

  • WAF の Logging and metrics タブから Logging を Enable にします
  • 作成した Firehose を選択します
  • Redacted fields で全てにチェックをつけます

以上で WAF へのリクエストログが全て S3 に保存されるはずです。

AWS Managed Rule をカウントのみで適用する

適当な AWS Managed Rule (例えば SQL database )を適用し Set rules action to count を有効にします。これでルールにヒットしたリクエストをカウントアップするだけで、実際にブロックしたりすることはありません。

例えば以下のようなリクエストをした際には、どこがルールにマッチしたのかをログに記録してくれるため調査がしやすくなります。

curl -d "999' and '1'='1'" http://sample-waf-alb-0000000000.ap-northeast-1.elb.amazonaws.com/

このリクエストは SQL インジェクションルールに引っかかるので、以下のような JSON でログに記録されます。 terminatingRuleMatchDetails にルールにマッチした詳細があります。(注意 : terminatingRuleMatchDetails に詳細が記録されるのは実際にブロックした場合のみです。カウントモードの場合は、詳細はわかりません)

{
   "timestamp":1580800929115,
   "formatVersion":1,
   "webaclId":"arn:aws:wafv2:ap-northeast-1:0000000000:regional/webacl/sample-waf/39252bd3-f769-431b-b73c-1ca5fa063f06",
   "terminatingRuleId":"AWS-AWSManagedRulesSQLiRuleSet",
   "terminatingRuleType":"MANAGED_RULE_GROUP",
   "action":"BLOCK",
   "terminatingRuleMatchDetails":[
      {
         "conditionType":"SQL_INJECTION",
         "location":"BODY",
         "matchedData":[
            "999",
            "and",
            "1",
            "=",
            "1"
         ]
      }
   ],
   "httpSourceName":"ALB",
   "httpSourceId":"0000000000-app/sample-waf-alb/3a239f6ed5af7d6d",
   "ruleGroupList":[
      {
         "ruleGroupId":"AWS#AWSManagedRulesSQLiRuleSet",
         "terminatingRule":{
            "ruleId":"SQLi_BODY",
            "action":"BLOCK"
         },
         "nonTerminatingMatchingRules":[

         ],
         "excludedRules":null
      }
   ],
   "rateBasedRuleList":[

   ],
   "nonTerminatingMatchingRules":[

   ],
   "httpRequest":{
      "clientIp":"000.000.000.000",
      "country":"JP",
      "headers":[
         {
            "name":"Host",
            "value":"sample-waf-alb-0000000000.ap-northeast-1.elb.amazonaws.com"
         },
         {
            "name":"Content-Length",
            "value":"15"
         },
         {
            "name":"User-Agent",
            "value":"curl/7.54.0"
         },
         {
            "name":"Accept",
            "value":"*/*"
         },
         {
            "name":"Content-Type",
            "value":"application/x-www-form-urlencoded"
         }
      ],
      "uri":"REDACTED",
      "args":"REDACTED",
      "httpVersion":"HTTP/1.1",
      "httpMethod":"REDACTED",
      "requestId":null
   }
}

Athena でログを集計する

これで S3 にログが溜まったので次は Athena を使ってログを集計できるようにします。

CREATE EXTERNAL TABLE IF NOT EXISTS waflogs2 (
  `timestamp` bigint,
  `formatVersion` int,
  `webaclId` string,
  `terminatingRuleId` string,
  `terminatingRuleType` string,
  `action` string,
  `terminatingRuleMatchDetails` array <
    struct <
      conditionType: string,
      location: string,
      matchedData: array < string >
    >
  >,
  `httpSourceName` string,
  `httpSourceId` string,
  `ruleGroupList` array <
    struct <
      ruleGroupId: string,
      terminatingRule: struct < ruleId: string, action: string >,
      nonTerminatingMatchingRules: array < struct < action: string, ruleId: string > >,
      excludedRules: array < struct < exclusionType: string, ruleId: string > >
    >
  >,
  `rateBasedRuleList` array <
    struct <
      rateBasedRuleId: string,
      limitKey: string,
      maxRateAllowed: int
    >
  >,
  `nonTerminatingMatchingRules` array <
    struct < 
      action: string,
      ruleId: string
    >
  >,
  `httpRequest` struct <
      clientIp: string,
      country: string,
      headers: array <struct < name: string, value: string > >,
      uri: string,
      args: string,
      httpVersion: string,
      httpMethod: string,
      requestId: string
  >
)
PARTITIONED BY (year STRING, month STRING, day STRING)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://bucket-name/'

これでテーブルを作成します。パーティションは適当に year/month/day で指定していますが、そこはよしなにやりましょう。

あとは指定ルールにマッチしたリクエストを検索します。 nonTerminatingMatchingRules フィールドにマッチしたルールが入ります。

SELECT
	timestamp,
	httpRequest,
	nonTerminatingMatchingRules
FROM
	waflogs,
	UNNEST(nonTerminatingMatchingRules) t(nonTerminate)
WHERE nonTerminate.action = 'COUNT';

ただし、前述したようにカウントモードの場合は、詳細なマッチ理由はログに残らないので、アプリケーションログを付き合わせながらルールの適用具合を調査する必要があります。そこが面倒ですね…。でもこれで少なくとも事前調査はできるようになったかと思います。