CloudFormationでAWS WAFのログをCloudWatchLogsに出力してみた

CloudFormationでAWS WAFのログをCloudWatchLogsに出力してみました。AWS WAFのLoggingConfigurationやCloudWatchLogsのリソースベースポリシーの設定がハマりどころです。
2023.05.24

こんにちは!AWS事業本部のおつまみです。

みなさん!AWS WAFのWebACLトラフィックログは有効化していますか?
2023年5月時点でログは以下のサービスに出力することができるようになっています。

  • CloudWatch Logs
  • S3
  • Kinesis

2021年11月より、CloudWatch Logs、S3に直接がログが出力できるようになっています。
詳細はこちらのブログをご確認ください。

今回はCloudFormationを利用し、AWS WAFのログをCloudWatchLogsに出力する環境を構築してみたいと思います。
いくつかハマった箇所もあったので、その点についても合わせてご紹介します。

S3に出力する環境方法を知りたい方はこちらのブログをご確認ください。

構成図

本記事で構築するAWS環境は以下の構成です。

背景が赤色の範囲が本記事で構築するリソースです。

  • AWS WAF(WebAcl)
  • CloudWatch Logs log group

背景が灰色の範囲は、既存環境のリソースです。
※作成方法は割愛します。

  • ALB
  • EC2

やってみた

実行するCloudFormationテンプレート

AWSTemplateFormatVersion: '2010-09-09'
Description: "create waf Resources"

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix:
    Type: String
    Default: "demo"
    Description: "Fill in the name of the system name."

  Env:
    Type: String
    Default: "test"
    Description: "Fill in the name of the environment."
  
  WebAclAssociationResourceArn:
    Type: String
    Default: "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:loadbalancer/app/XXXXXXXXXXXX"
    Description: Enter RegionalResource(ALB,APIGateway,AppSync) ARN or CloudFront ARN to associate with WEBACL. 

# ------------------------------------------------------------#
# create Resources
# ------------------------------------------------------------#
Resources:
  WebACL:
    Type: "AWS::WAFv2::WebACL"
    Properties:
      Name: !Sub ${PJPrefix}-${Env}-alb-acl
      Scope: "REGIONAL"
      DefaultAction:
        Allow: {}
      Description: "Web ACL for InternetALB"
      VisibilityConfig: 
          SampledRequestsEnabled: true
          CloudWatchMetricsEnabled: true
          MetricName: !Sub aws-waf-logs-${PJPrefix}-${Env}-alb-acl
      Rules:
        - Name: "AWS-AWSManagedRulesCommonRuleSet"
          Priority: 1
          OverrideAction:
            None: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: "AWS-AWSManagedRulesCommonRuleSet"
          Statement:
            ManagedRuleGroupStatement:
              Name: "AWSManagedRulesCommonRuleSet"
              VendorName: "AWS"

  WAFLogConfig:
    Type: AWS::WAFv2::LoggingConfiguration
    Properties:
      LogDestinationConfigs:
        - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-waf-logs-${PJPrefix}-${Env}-alb-acl
      ResourceArn: !GetAtt WebACL.Arn

  WebACLAssociation:
    Type: AWS::WAFv2::WebACLAssociation
    Properties: 
      ResourceArn: !Ref WebAclAssociationResourceArn
      WebACLArn: !GetAtt WebACL.Arn

  WAFLogGroup:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: !Sub aws-waf-logs-${PJPrefix}-${Env}-alb-acl
      RetentionInDays: 365
      Tags: 
        - Key: Name
          Value: !Sub aws-waf-logs-${PJPrefix}-${Env}-alb-acl

  WAFToCWLogsPolicy:
    Type: AWS::Logs::ResourcePolicy
    Properties:
      PolicyName: WAFToCWLogsPolicy
      PolicyDocument: 
        Fn::Sub:
          - |
            {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Principal": {
                    "Service": "delivery.logs.amazonaws.com"
                  },
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                  ],
                  "Resource": "${CloudWatchLogsLogGroupArn}",
                  "Condition": {
                    "StringEquals": {
                      "aws:SourceAccount": ${AWS::AccountId}
                    },
                    "ArnLike": {
                      "aws:SourceArn": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
                    }
                  }
                }
              ]
            }
          - CloudWatchLogsLogGroupArn: !GetAtt WAFLogGroup.Arn

今回テンプレート内で作成するリソースは以下です。

  • AWS WAF
    • WebACL
    • LoggingConfiguration(ログ出力先の設定)
    • WebACLAssociation(Web Aclに関連付けするリソースの設定)
  • CloudWatchLogs
    • LogGroup
    • ResourcePolicy

各リソースについて、簡単に解説します。

AWS WAF

WebAcl

WAFはWeb Aclという単位でリソースの作成、設定を行います。
ルールは今回AWS提供のマネージドルールAWS-AWSManagedRulesCommonRuleSetを設定しました。要件に応じて変更してください。

  WebACL:
    Type: "AWS::WAFv2::WebACL"
    Properties:
      Name: !Sub ${PJPrefix}-${Env}-alb-acl
      Scope: "REGIONAL"
      DefaultAction:
        Allow: {}
      Description: "Web ACL for InternetALB"
      VisibilityConfig: 
          SampledRequestsEnabled: true
          CloudWatchMetricsEnabled: true
          MetricName: !Sub aws-waf-logs-${PJPrefix}-${Env}-alb-acl
      Rules:
        - Name: "AWS-AWSManagedRulesCommonRuleSet"
          Priority: 1
          OverrideAction:
            None: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: "AWS-AWSManagedRulesCommonRuleSet"
          Statement:
            ManagedRuleGroupStatement:
              Name: "AWSManagedRulesCommonRuleSet"
              VendorName: "AWS"

CloudFormationの記述方法は下記を参考にしています。

AWS::WAFv2::WebACL - AWS CloudFormation

AWS::WAFv2::WebACL Rule - AWS CloudFormation

LoggingConfiguration

WAFのWebACLトラフィックログ出力先の設定を行います。
LogDestinationConfigsの記述方法で1ハマりしたので、後半で解説します。

  WAFLogConfig:
    Type: AWS::WAFv2::LoggingConfiguration
    Properties:
      LogDestinationConfigs:
        - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-waf-logs-${PJPrefix}-${Env}-alb-acl
      ResourceArn: !GetAtt WebACL.Arn

CloudFormatinonの記述方法は下記を参考にしています。

AWS::WAFv2::LoggingConfiguration - AWS CloudFormation

WebACLAssociation

Web Aclに関連付けするリソースの設定を行います。
今回は既に構築済のALBのARNをParameterで指定し、ResourceArnで関連付けています。

  WebACLAssociation:
    Type: AWS::WAFv2::WebACLAssociation
    Properties: 
      ResourceArn: !Ref WebAclAssociationResourceArn
      WebACLArn: !GetAtt WebACL.Arn

CloudFormatinonの記述方法は下記を参考にしています。

AWS::WAFv2::WebACLAssociation - AWS CloudFormation

CloudWatchLogs

LogGroup

WebACLトラフィックログ出力先となるCloudWatchLogs Loggroupを作成します。
RetentionInDaysで保持期間を設定しています。 今回は365(1年)で設定していますが、コストに関係する部分であるため、必要に応じて設定してください。

  WAFLogGroup:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: !Sub aws-waf-logs-${PJPrefix}-${Env}-alb-acl
      RetentionInDays: 365
      Tags: 
        - Key: Name
          Value: !Sub aws-waf-logs-${PJPrefix}-${Env}-alb-acl

CloudFormatinonの記述方法は下記を参考にしています。

AWS::Logs::LogGroup - AWS CloudFormation

ResourcePolicy

WAFのWebACLトラフィックログを出力するために、CloudWatchLogsに与えるリソースベースポリシーの作成を行います。
当初はこのポリシーを作成しておらず、ハマりしました 。。(2ハマり目)
詳細は後半で後半で解説します。

  WAFToCWLogsPolicy:
    Type: AWS::Logs::ResourcePolicy
    Properties:
      PolicyName: WAFToCWLogsPolicy
      PolicyDocument: 
        Fn::Sub:
          - |
            {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Principal": {
                    "Service": "delivery.logs.amazonaws.com"
                  },
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                  ],
                  "Resource": "${CloudWatchLogsLogGroupArn}",
                  "Condition": {
                    "StringEquals": {
                      "aws:SourceAccount": ${AWS::AccountId}
                    },
                    "ArnLike": {
                      "aws:SourceArn": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
                    }
                  }
                }
              ]
            }
          - CloudWatchLogsLogGroupArn: !GetAtt WAFLogGroup.Arn

CloudFormatinonの記述方法は下記を参考にしています。

AWS::Logs::ResourcePolicy - AWS CloudFormation

CloudFormation実行

今回はマネジメントコンソール経由でCloudFormationテンプレートを実行します。
リージョンは東京リージョンを指定し、テンプレートをアップロードします。

パラメータは以下のように設定します。

説明
スタックの名前 任意の名前
Env AWSリソースの接頭語として付与できる文字列(環境名)を設定します。
Prefix AWSリソースの接頭語として付与できる文字列(プロジェクト名など)を設定します。
WebAclAssociationResourceArn WebAclに関連付けするAWSリソースのARNを指定します。AWSリソースの例) ALB,Cloudfront,API Gateway,AppSync

スタックの作成を実行し、実行結果がCREATE_COMPLETEになっていることを確認します。

リソースの確認

テンプレートで定義したリソースが正常に作成・設定できているか確認します。

WAF

WAF & Shield コンソール で Web Aclを選択します。
今回は東京リージョンで作成したので、リージョン選択欄にAsia Pacific (Tokyo)を指定します。
作成されているWAFを選択します。

各タブを確認し、設定が適切であることを確認します。

Rules

Associated AWS resources

Logging and metrics

CloudWatchLogs

WAFのログ設定で設定されているLogGroupを選択します。
リソースベースポリシーのアクセス許可が正しく設定されていれば、ログストリームが出力されています。

設定は以上となりますが、今回ハマった箇所についてもお伝えします。

ハマりどころ

LogDestinationConfigsの指定方法

AWS WAFのログの出力先設定LoggingConfigurationでは、Web ACLに関連付けたいログ設定としてLogDestinationConfigsを設定しています。
当初は下記のように設定していました。

  WAFLogConfig:
    Type: AWS::WAFv2::LoggingConfiguration
    Properties:
      LogDestinationConfigs:
        - !GetAtt WAFLogGroup.Arn
      ResourceArn: !GetAtt WebACL.Arn

この状態でテンプレートをを実行すると、LogGroupが*となり、関連付けできませんでした。

*を選択すると、下記のページにとびます。

エラーメッセージ(訳)

1 validation error detected: Value '*' at 'logGroupName' failed to satisfy constraint: Member must satisfy regular expression pattern: [\.\-_/#A-Za-z0-9]+ (1 つの検証エラーが検出されました: 'logGroupName' の値 '*' が制約を満たすことができませんでした: メンバーは正規表現パターンを満たす必要があります: [\.\-_/#A-Za-z0-9]+)

どうやらLogDestinationConfigs:で直接LogGroupのARNを指定すると末尾に:*が付いてしまうことが原因でした。
というのもLogGroupのARNはarn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:aws-waf-logs-log-group:*となっています。

そのため、LogGroupのARNは下記のように文字列結合する必要があります。

  WAFLogConfig:
    Type: AWS::WAFv2::LoggingConfiguration
    Properties:
      LogDestinationConfigs:
        - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-waf-logs-${PJPrefix}-${Env}-alb-acl
      ResourceArn: !GetAtt WebACL.Arn

リソースベースポリシーの設定漏れ

AWS WAFのログをCloudWatachLogsに出力するためには、リソースベースポリシーが必要となります。
リソースベースポリシーとは、その名の通り操作対象のリソースに対して設定するポリシーです。
当初はこのポリシーを設定していなかったため、設定したCloudWatachLogsにログストリームが出力されませんでした。

そのため、リソースポリシーの設定を追加しました。
なおPolicyDocumentStringで記述する必要があるため、書き方に注意してください。

  WAFToCWLogsPolicy:
    Type: AWS::Logs::ResourcePolicy
    Properties:
      PolicyName: WAFToCWLogsPolicy
      PolicyDocument: 
        Fn::Sub:
          - |
            {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Principal": {
                    "Service": "delivery.logs.amazonaws.com"
                  },
                  "Action": [
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                  ],
                  "Resource": "${CloudWatchLogsLogGroupArn}",
                  "Condition": {
                    "StringEquals": {
                      "aws:SourceAccount": ${AWS::AccountId}
                    },
                    "ArnLike": {
                      "aws:SourceArn": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
                    }
                  }
                }
              ]
            }
          - CloudWatchLogsLogGroupArn: !GetAtt WAFLogGroup.Arn

なおCloudFormationでリソースベースポリシーが記述できるようになったのは、2021年7月ごろからのようです。

注意点として、CloudFormationで作成できるリソースベースポリシーはAWSアカウントの各リージョンにつき10個までという制限があります。
下記CloudFormationガイドより引用

AWS::Logs::ResourcePolicy - AWS CloudFormation

An account can have up to 10 resource policies per AWS Region.(アカウントには、AWS リージョンごとに最大 10 個のリソース ポリシーを含めることができます。)

そのため、リソースベースポリシーを量産する場合はResourceでワイルドカード(*)を利用したり、List形式で列挙するなど工夫が必要となります。

なお設定したリソースベースポリシーは下記のコマンドで確認できます。

aws logs describe-resource-policies --region ap-northeast-1

最後に

今回はAWS WAFのログをCloudWatchLogsに出力するCloudFormationで構築してみました。

手動で設定すると上手くいくCloudWatchLogとの関連付けや自動で作成されるリソースベースポリシーなどにハマりましたが、なんとか構築することができました。
同じようにハマってしまった方の参考になれば嬉しいです。

最後までお読みいただきありがとうございました!

以上、おつまみ(@AWS11077)でした!

参考

[アップデート] AWS WAFのログを直接CloudWatch LogsおよびS3に出力可能になりました | DevelopersIO

CloudFormationでAWS WAFを構築してみた(2022年1月版) | DevelopersIO

AWS WAF のログ記録をオンにして、CloudWatch、Amazon S3、または Kinesis Data Firehose にログを送信するにはどうすればよいですか? | AWS re:Post