CloudFormation で AWS WAF のログを S3 バケットに保存する

2020年5月9日

概要

やりたいこと

AWS WAF の検知ログを S3 に保存したい。

AWS WAF v2 リソースは既に作成されてるものとする。

Kinesis Firehose の IAM Role

AWS WAF ログを S3 に保存するには Kinesis Firehose を利用する必要がある。

Kinesis Firehose には S3 へのアクセス許可を付与する必要がある。
その方法が2種類ある。今回は 1 を採用。

  1. Amazon S3 の送信先へのアクセス権を Kinesis Data Firehose に付与する
  2. ウェブ ACL トラフィック情報のログ記録
    • iam:CreateServiceLinkedRole
    • firehose:ListDeliveryStreams
    • wafv2:PutLoggingConfiguration

1 の方で行う場合、2 種類の Role を付与する。

CloudFormation でデプロイ

準備

こちら参照。

S3 バケット

最初にログを貯める S3 バケットを作成しておく。

AWSTemplateFormatVersion: 2010-09-09
Description: Create WebACL example
Parameters:
  BucketName:
    Type: String
    Description: The name for the bucket.
    Default: waf-logs
Resources:
  S3BacketForWAF:
    Type: "AWS::S3::Bucket"
    Properties: 
      BucketName: !Sub waf-logs-${AWS::AccountId}
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256 
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

IAM Role 作成

S3 バケットを保存するための IAM Role を作成する。内容は

  • Kinesis Firehose に Role を付与するための sts:AssumeRole
  • S3 にアクセス許可を与える firehose_delivery_role

sts:AssumeRole

sts:AssumeRole を付与。

AWSTemplateFormatVersion: 2010-09-09
Description: Create WebACL example
Parameters:
  WebACLName:
    Description: The Name of WebACL.
    Type: String
    Default: ExampleWebACL
  BucketName:
    Type: String
    Description: The name for the bucket.
    Default: waf-logs
Resources:
  S3BacketForWAF:
    Type: "AWS::S3::Bucket"
    Properties: 
      BucketName: !Sub ${BucketName}-${AWS::AccountId}
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256 
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
  FirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      Path: '/'
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - firehose.amazonaws.com
            Action:
              - sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Sub ${AWS::AccountId}

S3アクセス用ポリシー firehose_delivery_role

以下のポリシーを作成している。

  • S3 バケットへのアクセス許可
  • Kinesis へのアクセス許可
  • S3 のデータ暗号化
  • Kinesis によるログ保存イベントの許可
  • Lambda Function の許可(CloudWatch用?)
  • AWS KMS キー使用のポリシー
AWSTemplateFormatVersion: 2010-09-09
Description: Create WebACL example
Parameters:
  WebACLName:
    Description: The Name of WebACL.
    Type: String
    Default: ExampleWebACL
  BucketName:
    Type: String
    Description: The name for the bucket.
    Default: waf-logs
Resources:
  S3BacketForWAF:
    Type: "AWS::S3::Bucket"
    Properties: 
      BucketName: !Sub ${BucketName}-${AWS::AccountId}
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256 
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
  FirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      Path: '/'
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - firehose.amazonaws.com
            Action:
              - sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Sub ${AWS::AccountId}
      Policies:
        - 
         PolicyName: firehose_delivery_role
         PolicyDocument:
           Version: "2012-10-17"
           Statement:
              -
                Effect: "Allow"
                Action: 
                  - s3:AbortMultipartUpload
                  - s3:GetBucketLocation
                  - s3:GetObject
                  - s3:ListBucket
                  - s3:ListBucketMultipartUploads
                  - s3:PutObject
                Resource:
                  - !Sub arn:aws:s3:::${BucketName}-${AWS::AccountId}
                  - !Sub arn:aws:s3:::${BucketName}-${AWS::AccountId}/*
                  - arn:aws:s3:::%FIREHOSE_BUCKET_NAME%
                  - arn:aws:s3:::%FIREHOSE_BUCKET_NAME%/*
              -
                Effect: "Allow"
                Action: 
                  - kinesis:DescribeStream
                  - kinesis:GetShardIterator
                  - kinesis:GetRecords
                Resource:
                  - !Sub arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:stream/%FIREHOSE_STREAM_NAME%
              -
                Effect: "Allow"
                Action: 
                  - kms:GenerateDataKey
                  - kms:Decrypt
                Resource:
                  - !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3
                Condition:
                  StringEquals:
                    kms:ViaService: !Sub s3.${AWS::Region}.amazonaws.com
                  StringLike:
                    kms:EncryptionContext:aws:s3:arn:
                      - !Sub arn:aws:s3:::${BucketName}-${AWS::AccountId}/*
                      - !Sub arn:aws:s3:::${BucketName}-${AWS::AccountId}/%FIREHOSE_BUCKET_PREFIX%*
              -
                Effect: "Allow"
                Action: 
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kinesisfirehose/aws-waf-logs-${AWS::Region}-${WebACLName}:log-stream:*
              -
                Effect: "Allow"
                Action: 
                  - lambda:InvokeFunction
                  - lambda:GetFunctionConfiguration
                Resource:
                  - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:%FIREHOSE_DEFAULT_FUNCTION%:%FIREHOSE_DEFAULT_VERSION%

Kinesis Firehose

AWS::KinesisFirehose::DeliveryStream を利用し、ログの保存先とログ記録を有効にする WebACL を指定する。

注意として、 DeliveryStreamName は「aws-waf-logs-」をプレフィックスとして付けなければならない。後述の WAF Logging 設定時に、選択肢に作成した Kinesis が表示されないため。公式で紹介されていた。

AWSTemplateFormatVersion: 2010-09-09
Description: Create WebACL example
Parameters:
  WebACLName:
    Description: The Name of WebACL.
    Type: String
    Default: ExampleWebACL
  BucketName:
    Type: String
    Description: The name for the bucket.
    Default: waf-logs
Resources:
  S3BacketForWAF:
    Type: "AWS::S3::Bucket"
    Properties: 
      BucketName: !Sub ${BucketName}-${AWS::AccountId}
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256 
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
  FirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      Path: '/'
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - firehose.amazonaws.com
            Action:
              - sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Sub ${AWS::AccountId}
      Policies:
        - 
         PolicyName: firehose_delivery_role
         PolicyDocument:
           Version: "2012-10-17"
           Statement:
              -
                Effect: "Allow"
                Action: 
                  - s3:AbortMultipartUpload
                  - s3:GetBucketLocation
                  - s3:GetObject
                  - s3:ListBucket
                  - s3:ListBucketMultipartUploads
                  - s3:PutObject
                Resource:
                  - !Sub arn:aws:s3:::${BucketName}-${AWS::AccountId}
                  - !Sub arn:aws:s3:::${BucketName}-${AWS::AccountId}/*
                  - arn:aws:s3:::%FIREHOSE_BUCKET_NAME%
                  - arn:aws:s3:::%FIREHOSE_BUCKET_NAME%/*
              -
                Effect: "Allow"
                Action: 
                  - kinesis:DescribeStream
                  - kinesis:GetShardIterator
                  - kinesis:GetRecords
                Resource:
                  - !Sub arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:stream/%FIREHOSE_STREAM_NAME%
              -
                Effect: "Allow"
                Action: 
                  - kms:GenerateDataKey
                  - kms:Decrypt
                Resource:
                  - !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3
                Condition:
                  StringEquals:
                    kms:ViaService: !Sub s3.${AWS::Region}.amazonaws.com
                  StringLike:
                    kms:EncryptionContext:aws:s3:arn:
                      - !Sub arn:aws:s3:::${BucketName}-${AWS::AccountId}/*
                      - !Sub arn:aws:s3:::${BucketName}-${AWS::AccountId}/%FIREHOSE_BUCKET_PREFIX%*
              -
                Effect: "Allow"
                Action: 
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kinesisfirehose/aws-waf-logs-${AWS::Region}-${WebACLName}:log-stream:*
              -
                Effect: "Allow"
                Action: 
                  - lambda:InvokeFunction
                  - lambda:GetFunctionConfiguration
                Resource:
                  - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:%FIREHOSE_DEFAULT_FUNCTION%:%FIREHOSE_DEFAULT_VERSION%
  FirehoseDeliveryStream:
    Type: "AWS::KinesisFirehose::DeliveryStream"
    Properties:
      DeliveryStreamName: !Sub aws-waf-logs-${BucketName}-${AWS::Region}-${WebACLName}
      DeliveryStreamType: DirectPut
      S3DestinationConfiguration:
        BucketARN: !GetAtt S3BacketForWAF.Arn
        BufferingHints:
          IntervalInSeconds: 300
          SizeInMBs: 5
        CompressionFormat: GZIP
        RoleARN: !GetAtt FirehoseRole.Arn

構文チェック後に反映。

aws cloudformation validate-template --template-body file://waf_v2.yaml
aws cloudformation deploy --stack-name aws-waf-v2-test --template-file waf_v2.yaml --capabilities CAPABILITY_NAMED_IAM

Kinesis Data Firehose コンソールより作成されていることを確認。

AWS WAF 設定

AWS::WAFv2::WebACL を確認してもロギング設定をするパラメータが見当たらない。。AWS WAFコンソールから手動で設定しないといけないようだ。。

  1. 作成された WebACL を選択
  2. Logging and metrics
  3. Enable logging
  4. Amazon Kinesis Data Firehose Delivery Stream
    • 上記で作成したもの
  5. Redacted fields
    • お好みのもの
  6. Enable Logging

設定完了後、上手く動いていれば S3 バケットにログが保存されている。

中身は巨大な json。

{"timestamp":1589003189188,"formatVersion":1,"webaclId":"arn:aws:wafv2:ap-northeast-1:657885203613:regional/webacl/ExampleWebACL/08f08fc4-8cd1-4f7c-af58-c21b181d93e6","terminatingRuleId":"Default_Action","terminatingRuleType":"REGULAR","action":"ALLOW","terminatingRuleMatchDetails":[],"httpSourceName":"ALB","httpSourceId":"657885203613-app/webserver-alb/3c8d60490de0199e","ruleGroupList":[{"ruleGroupId":"AWS#AWSManagedRulesCommonRuleSet","terminatingRule":null,"nonTerminatingMatchingRules":[],"excludedRules":null},{"ruleGroupId":"AWS#AWSManagedRulesKnownBadInputsRuleSet","terminatingRule":null,"nonTerminatingMatchingRules":[],"excludedRules":null},{"ruleGroupId":"AWS#AWSManagedRulesAmazonIpReputationList","terminatingRule":null,"nonTerminatingMatchingRules":[],"excludedRules":null},{"ruleGroupId":"AWS#AWSManagedRulesAnonymousIpList","terminatingRule":null,"nonTerminatingMatchingRules":[],"excludedRules":null},{"ruleGroupId":"AWS#AWSManagedRulesSQLiRuleSet","terminatingRule":null,"nonTerminatingMatchingRules":[],"excludedRules":null},{"ruleGroupId":"AWS#AWSManagedRulesLinuxRuleSet","terminatingRule":null,"nonTerminatingMatchingRules":[],"excludedRules":null},{"ruleGroupId":"AWS#AWSManagedRulesUnixRuleSet","terminatingRule":null,"nonTerminatingMatchingRules":[],"excludedRules":null}],"rateBasedRuleList":[],"nonTerminatingMatchingRules":[],"httpRequest":{"clientIp":"111.108.92.1","country":"JP","headers":[{"name":"Host","value":"test-security-aws.net"},{"name":"Pragma","value":"no-cache"},{"name":"Cache-Control","value":"no-cache"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"},{"name":"Origin","value":"https://test-security-aws.net"},{"name":"Sec-WebSocket-Version","value":"13"},{"name":"Accept-Encoding","value":"gzip, deflate, br"},{"name":"Accept-Language","value":"ja,en-US;q=0.9,en;q=0.8"},{"name":"Cookie","value":"language=en; welcomebanner_status=dismiss; io=qD01R6-qBXefEj1bAAAP"},{"name":"Sec-WebSocket-Key","value":"gjOonPgUgqiZPcPKW1EZkg=="},{"name":"Sec-WebSocket-Extensions","value":"permessage-deflate; client_max_window_bits"}],"uri":"REDACTED","args":"REDACTED","httpVersion":"HTTP/1.1","httpMethod":"REDACTED","requestId":null}}

コードは github にまとめてある。

An error occurred (InsufficientCapabilitiesException) when calling the CreateChangeSet operation: Requires capabilities : [CAPABILITY_IAM]

Kinesis と IAM の設定反映の際にエラーが出てしまった。

$ aws cloudformation deploy --stack-name aws-waf-v2-test --template-file waf_v2.yaml
An error occurred (InsufficientCapabilitiesException) when calling the CreateChangeSet operation: Requires capabilities : [CAPABILITY_IAM]

CloudFormation で「AWS::IAM::Role」Type を利用する場合、--capabilities パラメータに CAPABILITY_IAM または CAPABILITY_NAMED_IAM 値を指定する必要がある。

CAPABILITY_IAM または CAPABILITY_NAMED_IAM の使い分けは以下。

  • CAPABILITY_IAM : テンプレートの IAM リソースにカスタム名がない場合
  • CAPABILITY_NAMED_IAM : テンプレートの IAM リソースにカスタム名がある場合

今回はKinesis 用の IAM ポリシー(firehose_delivery_role)を作成するため、CAPABILITY_NAMED_IAM を利用する。
スタック作成時に

aws cloudformation create-stack --stack-name aws-waf-v2-test --region ap-northeast-1 --template-body file://waf_v2.yaml --capabilities CAPABILITY_NAMED_IAM

または、デプロイ時に--capabilities パラメータを指定する。

aws cloudformation deploy --stack-name aws-waf-v2-test --template-file waf_v2.yaml --capabilities CAPABILITY_NAMED_IAM

参考

AWS WAFのLogを保存するKinesis FirehoseをCloudFormationでさくっと作ってみた

AWS Managed Rules for AWS WAF を構成するCloudFormationテンプレートを作ってみた

Terraform で AWS WAF のログを Kinesis Firehose 経由で logging する

when calling the CreateStack operation: Requires capabilities : [CAPABILITY_IAM]エラーの対応