ブログ

AWS WAF のログは事前に準備しましょう

最近需要が多く、マネージドルールを適用することで簡単にWebサイトに AWS WAF を導入できます。

導入自体は簡単ですが、意外と大変なのが導入した後 AWS WAF で通信を遮断した(したかもしれない)場合に、AWS WAF のログ調査から始まるというケースです。こういった場面を経験される方も多いのではないでしょうか?

何か起きてからでは調査が難航したりできなかったりするため、今回は調査時に必要なログの取得方法や確認方法をご紹介します。

AWS WAF ログの取得方法

まずは、AWS WAF のログを取得する設定からご紹介します。

AWS WAF ログを取得するには「Logging and metrics」から Logging の「Enable」を押下し、

「S3 bucket」を選択し、出力先の Amazon S3 バケットを指定します。
画面上でも案内がありますが、「aws-waf-logs-」から始まるS3バケット名である必要があります。

※今回は S3 バケットに直接出力する方法を選択していますが、CloudWatch Logs 等でも可能です。

実際にログが取得されるとこのような階層に出力されます。

AWS WAF ログの確認方法

S3 バケットに保存されたファイルをそのまま確認することもできますが、ファイル数が多くなると確認が大変なため、Amazon Athena を利用して SQL で AWS WAF のログを確認できるようにします。

Athena で、下記 SQL を実行します。

CREATE EXTERNAL TABLE `waf_logs`(
    `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,
    rulematchdetails :string >,
    nonterminatingmatchingrules :array < struct < ruleid :string,
    action :string,
    rulematchdetails :array < struct < conditiontype :string,
    location :string,
    matcheddata :array < string > > > > >,
    excludedrules :array < struct < ruleid :string,
    exclusiontype :string > > > >,
    `ratebasedrulelist` array < struct < ratebasedruleid :string,
    ratebasedrulename :string,
    limitkey :string,
    limitvalue :string,
    maxrateallowed :int > >,
    `nonterminatingmatchingrules` array < struct < ruleid :string,
    action :string,
    rulematchdetails :array < struct < conditiontype :string,
    location :string,
    matcheddata :array < string > > >,
    captcharesponse :struct < responsecode :string,
    solvetimestamp :bigint > > >,
    `requestheadersinserted` string,
    `responsecodesent` string,
    `httprequest` struct < clientip :string,
    country :string,
    headers :array < struct < name :string,
    value :string > >,
    uri :string,
    args :string,
    httpversion :string,
    httpmethod :string,
    requestid :string >,
    `labels` array < struct < name :string > >,
    `captcharesponse` struct < responsecode :string,
    solvetimestamp :bigint,
    failurereason :string >
) ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' WITH SERDEPROPERTIES (
    'paths' = 'action,formatVersion,httpRequest,httpSourceId,httpSourceName,labels,nonTerminatingMatchingRules,rateBasedRuleList,requestHeadersInserted,responseCodeSent,ruleGroupList,terminatingRuleId,terminatingRuleMatchDetails,terminatingRuleType,timestamp,webaclId,captchaResponse'
) STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' 
 LOCATION 's3://[S3バケット名]/AWSLogs/[AWSアカウントID]/WAFLogs/cloudfront/[WebACL名]/'

成功メッセージとともに、左側に新しくテーブル(今回はwaf_logs)が追加されます。
これで SQL を使用した AWS WAF ログの確認ができるようになります。

ここで、個人的によく利用する SQL をご紹介します。

タイムスタンプ(timestamp)をJSTに変換

実際にログ調査をする場合、リクエスト日時を見ることが多いかと思いますが、timestamp では、ぱっと見ただけではリクエスト日時の判断ができません。

その場合は、from_unixtime() を使用してJSTに変換します。

select
    from_unixtime(timestamp / 1000, 'Asia/Tokyo') as jst,
    *
from
    waf_logs

JST で期間指定して抽出

ログ出力結果を JST で欲しい場合、抽出もJSTでしたい場合がほとんどだと思います。

その場合は、以下のような SQL で抽出可能です。

select
    from_unixtime(timestamp / 1000, 'Asia/Tokyo') as jst,
    *
from
    waf_logs
where
    from_unixtime(timestamp / 1000, 'Asia/Tokyo') >= timestamp '2021-02-24 00:00:00 Asia/Tokyo'
    and from_unixtime(timestamp / 1000, 'Asia/Tokyo') <= timestamp '2021-02-28 23:59:59 Asia/Tokyo'

期間内の IP アドレス毎のリクエスト数

DDoS 攻撃の疑いがある場合、IP アドレス毎のリクエスト数を確認して飛び抜けたものだけ対策等をするかと思います。

その場合は、以下のような SQL で集計可能です。

SELECT
    httprequest.clientip,
    count(*) as "RequestCount"
FROM
    waf_logs
WHERE
    from_unixtime(timestamp / 1000, 'Asia/Tokyo') >= timestamp '2021-02-24 00:00:00 Asia/Tokyo'
    and from_unixtime(timestamp / 1000, 'Asia/Tokyo') <= timestamp '2021-02-28 23:59:59 Asia/Tokyo'
group by
    httprequest.clientip
order by
    "RequestCount" desc

また、日毎に分けて集計したい場合は、以下のようにします。

select
    JST,
    clientip,
    count(*) as "RequestCount"
from
    (
        SELECT
            substr(cast(from_unixtime(timestamp / 1000, 'Asia/Tokyo') AS VARCHAR),1,10) AS JST,
            httprequest.clientip AS clientip
        FROM
            waf_logs
        WHERE
            from_unixtime(timestamp / 1000, 'Asia/Tokyo') >= timestamp '2021-02-24 00:00:00 Asia/Tokyo'
            and from_unixtime(timestamp / 1000, 'Asia/Tokyo') <= timestamp '2021-02-28 23:59:59 Asia/Tokyo'
    ) as date_and_ip
group by
    JST,
    clientip
order by
    JST asc,
    "RequestCount" desc

X-forwarded-For に特定 IP が含まれるログを抽出

通常クライアントIP は「httprequest.clientip」を確認すればよいのですが、X-forwarded-For ヘッダーにクライアントIPが入っている場合、取得方法が以下のように変わります。

select
    *
from
    waf_logs,
    UNNEST(httprequest.headers) t(header)
where
    header.name = 'X-Forwarded-For'
    and header.value like '%[IPアドレス(プリフィックス値無し)]%'

AWS WAF ログと Apache ログの連携方法

AWS WAF のログ自体の確認は Athena で完結しますが、1リクエスト単位で AWS WAF ログと Apache ログを紐付けて確認したい場合もあるかと思います。

そんな時は、Apache ログのフォーマット(LogFormatディレクティブ)を変更することで紐付け可能です。

CloudFront に AWS WAF が適用されている場合は、LogFormat に「 %{x-amz-cf-id}i」を指定し、ALB に AWS WAF が適用されている場合は、LogFormat に「 %{X-Amzn-Trace-Id}i」を指定してください。

そうすることで Apache ログにリクエストID(トレースID)が記録されますので、AWS WAF ログの「httprequest.requestid」と突き合わせることでログ同士を紐付けることが可能です。

AWS WAF ログ(まとめ)

本ブログでは、筆者がまず取得・準備しておくべきと感じた内容を今回ご紹介いたしました。

今回ご紹介したログ調査をはじめ、ターン・アンド・フロンティアでは AWS をご利用されているお客様に技術的なご支援が可能ですので、AWS WAF の活用やログ取得にご興味のある方はお気軽にご相談ください。

元記事発行日: 2022年06月27日、最終更新日: 2022年06月27日