AWS WAFログのブロック時のみ出力やマスキングをCloudFormationで構築する

概要

AWS WAFのログで、以下を満たすようにCloudFormationで構築します。

  • アクセスのブロック時にのみログ出力する
    • 料金節約やログの視認性のため
  • ヘッダーなどの情報をマスキングする


WAFログ

従来

AWS WAFのログを出力するためには、今まではAmazon Kinesis Data Firehoseを使用して転送する必要がありました。

それが中々設定が面倒だったのですがつい最近、Firehose無しで直接WAFのログを出力することが可能になり、ずいぶん簡単にWAFのログを出力できるようになりました。


前提

WAFログの出力は、S3とCloudWatch Logs(とKinesis Data Firehose)が可能なのですが、本記事ではS3への出力を例に挙げます。

CloudWatch Logsへの出力も宛先を変更するだけでできます。


また上記で説明した、Firehose無しの方法でログ出力を行います。


コード

github.com

AWSTemplateFormatVersion: 2010-09-09

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Project Name"
        Parameters:
          - PJName
      - Label:
          default: "ApiGateway Resource ARN"
        Parameters:
          - ApiGatewayResourceArn

Parameters:
  PJName:
    Type: String
  ApiGatewayResourceArn:
    Type: String

Resources:
  ApiGatewayWebAclAssociation:
    Type: AWS::WAFv2::WebACLAssociation
    Properties:
      ResourceArn: !Ref ApiGatewayResourceArn
      WebACLArn: !GetAtt WAFWebACL.Arn

  WAFWebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      Name: !Sub "${PJName}-WebACL"
      DefaultAction:
        Block: {}
      Scope: "REGIONAL"
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        MetricName: !Sub "${PJName}-WebACL"
        SampledRequestsEnabled: true

  WAFLogsS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "aws-waf-logs-${PJName}"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  WAFLogConfig:
    Type: AWS::WAFv2::LoggingConfiguration
    Properties:
      LogDestinationConfigs:
        - !GetAtt WAFLogsS3Bucket.Arn
      ResourceArn: !GetAtt WAFWebACL.Arn
      LoggingFilter:
        DefaultBehavior: DROP
        Filters:
          - Behavior: KEEP
            Conditions:
              - ActionCondition:
                  Action: BLOCK
            Requirement: MEETS_ALL
      RedactedFields:
        - SingleHeader:
            Name: "authorization"


解説

WAFWebACL(WebACLAssociation)

これはWAFをリソースにアタッチするためのものですが、例としてAPI Gatewayにアタッチする想定にしています。

アタッチ対象のリソースのARNはパラメータとして渡しています。


WAFWebACL(WebACL)

こちらがWAF本体の設定になります。

サンプルなので全部ブロックする設定にしていますが、実際はRulesなどで特定のIPからのアクセスは許可するように設定します。

go-to-k.hatenablog.com


WAFLogsS3Bucket(S3バケット)

これはWAFのログを転送するS3のバケット自体です。


注意点として、WAFのログを出力するバケット名はaws-waf-logs-」で始まる必要があります。

でないと設定に失敗するため、忘れずに名前を決めましょう。

  WAFLogsS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "aws-waf-logs-${PJName}"


WAFLogConfig(WAF LoggingConfiguration)

今回の肝である、WAFログ出力のための設定です。

(今まではこれがなく、Firehoseとそのためのやたら長いIAMロールを代わりに構築していました。。。)

※WAFではなくAmazon SESのメール送信ログですが、同じようなFirehoseをCloudFormation構築しているので、良かったらご覧下さい。

go-to-k.hatenablog.com


ブロック時のみのログ出力

まずは、今回は要件にあったように、「ブロック時にのみログ出力する」ようにします。

それが以下の部分になります。

      LoggingFilter:
        DefaultBehavior: DROP
        Filters:
          - Behavior: KEEP
            Conditions:
              - ActionCondition:
                  Action: BLOCK
            Requirement: MEETS_ALL


DefaultBehavior: DROPにしておき、FiltersConditionsに当てはまるときはBehavior: KEEPする設定です。

Action: BLOCKのとき、つまり、アクセスがブロックされたときにのみログ出力が行われるようになります。


Requirement: MEETS_ALLというのは、List形式であるConditionsに指定した条件を全て満たすとき、という条件になります。

MEETS_ANYとすることで、指定した条件のどれかに当てはまるとき、というような条件にすることができます。

今回は一つの条件なのでどちらでも構いません。


マスキング (RedactedFields)

また、ログ内の特定の情報をマスキング(Redact=墨塗り)することができます。

CloudFormationでマスキングできる種類は以下になります。

  • UriPath
  • QueryString
  • SingleHeader
  • Method
  • JsonBody

詳しくは公式ドキュメントをご参照ください。

docs.aws.amazon.com


ここでは、「authorizationヘッダー」のマスキングをしてみます。

これでトークンなどの文字列をログに出力しないことができるようになります。

WAFのログには、基本的には本文(リクエストボディ)は出力されないため、今回はヘッダーを対象にしています。


      RedactedFields:
        - SingleHeader:
            Name: "authorization"

RedactedFieldsSingleHeaderを指定し、Name: "authorization"とすることで、このヘッダーの情報をマスキングすることができます。


実行

実際にブロック時のログの出力を確認してみます。

curl

domainにエンドポイントのURL、methodにはメソッド名を入れてcurlで叩きます。

リクエストボディは適当ですが、-H "authorization: abcdef"というようにauthorizationヘッダーに情報を格納して叩いています。

$ curl -i -X POST -H "Content-Type: application/json" -H "authorization: abcdef" -d '{"foo":"hoge"}' ${domain}/${method}

HTTP/2 403 
content-type: application/json
content-length: 23
date: Sat, 12 Mar 2022 17:02:06 GMT
x-amzn-requestid: abcdef-xxxx-xxxx-xxxx-abcdefghijk
x-amzn-errortype: ForbiddenException
...
...
...
{"message":"Forbidden"}


{"message":"Forbidden"}というように、とりあえずブロックされることが確認できました。


WAFログ

数分待ってS3にログが出力されるのを確認し、ファイルをダウンロード・解凍して開きます。

すると、以下のように、authorizationの値が"value":"REDACTED"というようにマスキングされて出力されました。

...,{"name":"Authorization","value":"REDACTED"},...


※authorizationの頭文字が大文字に変換されました (Authorization)。

※ログは長いので該当箇所のみ記載しています。


最後に

WAFのログ出力が簡単になったのはありがたいし、さらに出力の際に色々な設定ができるのは便利ですね。