CloudFormation で AWS WAF ログを Kinesis Firehose を経由して別アカウントの S3 バケットに送る(バケットポリシー)

2020年5月14日

概要

やりたいこと

前回、AWS WAF ログを S3 バケットに保存した。このログを別アカウントの S3 バケットに保存したい。

クロスアカウントアクセスというらしい。
状況を以下とする。

  • アカウント A : ここの S3 バケットに保存したい
  • アカウント B : AWS WAF が設定されている

クロスアカウントアクセス

公式より実現方法は 3 つある。今回は1の方法を採用。

  1. リソースベースのポリシーと AWS Identity and Access Management (IAM) ポリシー (S3 バケットオブジェクトへのプログラムによるアクセス専用)
  2. リソースベースのアクセスコントロールリスト (ACL) と IAM ポリシー (S3 バケットオブジェクトへのプログラムによるアクセス専用)
  3. クロスアカウント IAM ロール (S3 バケットオブジェクトへのプログラムおよびコンソールによるアクセス)

リソースベースポリシーと IAM ポリシー

クロスアカウントアクセスしたい場合、リソースベースポリシーと IAM ポリシー両方で許可しなければならない。

  • 単一アカウント内アクセス:IAMポリシーかリソースベースポリシーのどちらかの許可が必要
  • クロスアカウントアクセス:IAMポリシーとリソースベースポリシーの両方の許可が必要

今回のリソースベースのポリシーは S3 のバケットポリシーとなる。
AWS リソースごとのリソースベースポリシーの対応状況はこちら参照

CloudFormation

CloudFormation 準備

こちら参照。

アカウント A はコンソールより操作する。
アカウント B は CloudFormation で実行する。

mkdir waf-v2-basepolicy
cd waf-v2-basepolicy

スタック名も今回 aws-waf-v2-basepolicy とする。

アカウントA : S3 バケット作成

アカウント B にて、以下の名前で S3 バケットを作成。バケットポリシーは後ほど作成する。

  • waf-test-cross-account

アカウントB : Kinesis Data Firehose に IAM ロールを割り当てる

公式より、Kinesis Data Firehose に IAM ロールを割り当てるポリシーは以下。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "firehose.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId":"account-id"
        }
      }
    }
  ]
}

CloudFormation で作成する。

vi waf_v2.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Create WebACL example
Resources:
  FirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      Path: '/'
      AssumeRolePolicyDocument: #Kinesis Firehose に IAM を割り当てるため
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - firehose.amazonaws.com
            Action:
              - sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Sub ${AWS::AccountId}

スタック作成。

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

アカウントB : アカウント A の S3 バケットアクセス用の IAM ポリシー作成

公式より、Kinesis が S3 バケットへアクセスするのに必要なポリシーは以下。

ただし、クロスアカウントアクセスの場合、Action に “s3:PutObjectAcl” が必要なる。

{
    "Version": "2012-10-17",  
    "Statement":
    [    
        {      
            "Effect": "Allow",      
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:PutObject"
            ],      
            "Resource": [        
                "arn:aws:s3:::bucket-name",
                "arn:aws:s3:::bucket-name/*"		    
            ]    
        },        
        {
            "Effect": "Allow",
            "Action": [
                "kinesis:DescribeStream",
                "kinesis:GetShardIterator",
                "kinesis:GetRecords",
                "kinesis:ListShards"
            ],
            "Resource": "arn:aws:kinesis:region:account-id:stream/stream-name"
        },
        {
           "Effect": "Allow",
           "Action": [
               "kms:Decrypt",
               "kms:GenerateDataKey"
           ],
           "Resource": [
               "arn:aws:kms:region:account-id:key/key-id"           
           ],
           "Condition": {
               "StringEquals": {
                   "kms:ViaService": "s3.region.amazonaws.com"
               },
               "StringLike": {
                   "kms:EncryptionContext:aws:s3:arn": "arn:aws:s3:::bucket-name/prefix*"
               }
           }
        },
        {
           "Effect": "Allow",
           "Action": [
               "logs:PutLogEvents"
           ],
           "Resource": [
               "arn:aws:logs:region:account-id:log-group:log-group-name:log-stream:log-stream-name"
           ]
        },
        {
           "Effect": "Allow", 
           "Action": [
               "lambda:InvokeFunction", 
               "lambda:GetFunctionConfiguration" 
           ],
           "Resource": [
               "arn:aws:lambda:region:account-id:function:function-name:function-version"
           ]
        }
    ]
}

上記を CloudFormation で書いた firehose_delivery_policy を作成し、アタッチした IAM Role を作成する。

s3:PutObjectAcl を追加した。

AWSTemplateFormatVersion: 2010-09-09
Description: Create WebACL example
Parameters:
  BucketName:
    Type: String
    Description: The name for the bucket.
    Default: waf-test-cross-account
Resources:
  FirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      Path: '/'
      Policies:
        - 
         PolicyName: firehose_delivery_policy
         PolicyDocument:
           Version: "2012-10-17"
           Statement:
              -
                Effect: "Allow"
                Action: 
                  - s3:AbortMultipartUpload
                  - s3:GetBucketLocation
                  - s3:GetObject
                  - s3:ListBucket
                  - s3:ListBucketMultipartUploads
                  - s3:PutObject
                  - s3:PutObjectAcl
                Resource:
                  - !Sub arn:aws:s3:::${BucketName}
                  - !Sub arn:aws:s3:::${BucketName}/*
              -
                Effect: "Allow"
                Action: 
                  - kinesis:DescribeStream
                  - kinesis:GetShardIterator
                  - kinesis:GetRecords
                  - kinesis:ListShards
                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}/*
                      - !Sub arn:aws:s3:::${BucketName}/%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%
              -
                Effect: "Allow"
                Action: 
                  - kms:Decrypt
                Resource:
                  - !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/%SSE_KEY_ID%
                Condition:
                  StringEquals:
                    kms:ViaService: kinesis.%REGION_NAME%.amazonaws.com
                  StringLike:
                    kms:EncryptionContext:aws:kinesis:arn: !Sub arn:aws:kinesis:%REGION_NAME%:${AWS::AccountId}:stream/%FIREHOSE_STREAM_NAME%

構文チェックして反映。

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

IAM Role が作成され firehose_delivery_policy がアタッチされていることを確認。
この ARN は アカウントAの S3 バケットポリシーで利用するためメモしておく。

アカウントB : Kinesis Firehose を作成し、IAM Role と WebACL を紐付ける

CloudFormation 全体像。WebACLのルールは省略。

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-test-cross-account
Resources:
  FirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      Path: '/'
      AssumeRolePolicyDocument: # Kinesis FirehoseにIAM Roleを割り当てるため
        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
                  - s3:PutObjectAcl
                Resource:
                  - !Sub arn:aws:s3:::${BucketName}
                  - !Sub arn:aws:s3:::${BucketName}/*
              -
                Effect: "Allow"
                Action: 
                  - kinesis:DescribeStream
                  - kinesis:GetShardIterator
                  - kinesis:GetRecords
                  - kinesis:ListShards
                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}/*
                      - !Sub arn:aws:s3:::${BucketName}/%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%
              -
                Effect: "Allow"
                Action: 
                  - kms:Decrypt
                Resource:
                  - !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/%SSE_KEY_ID%
                Condition:
                  StringEquals:
                    kms:ViaService: kinesis.%REGION_NAME%.amazonaws.com
                  StringLike:
                    kms:EncryptionContext:aws:kinesis:arn: !Sub arn:aws:kinesis:%REGION_NAME%:${AWS::AccountId}:stream/%FIREHOSE_STREAM_NAME%
  FirehoseDeliveryStream:
    Type: "AWS::KinesisFirehose::DeliveryStream"
    Properties:
      DeliveryStreamName: !Sub aws-waf-logs-${BucketName}-${AWS::Region}-${WebACLName}
      DeliveryStreamType: DirectPut
      S3DestinationConfiguration:
        BucketARN: !Sub arn:aws:s3:::${BucketName}
        BufferingHints:
          IntervalInSeconds: 300
          SizeInMBs: 5
        CompressionFormat: GZIP
        RoleARN: !GetAtt FirehoseRole.Arn
  WebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      Name: !Sub ${WebACLName}
      Scope: REGIONAL
      Description: This is an example WebACL
      DefaultAction:
        Allow: {} # Allow or Block or Count
      VisibilityConfig:
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: !Sub ${WebACLName}

構文チェックして反映。

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

Kineis Firehose が作成され、作成した IAM がアタッチされていることを確認。

WebACL のLogging から Kinesis Firehose を有効化する。

アカウントA : バケットポリシー作成

公式より、以下のバケットポリシーを指定する必要があるとのこと。

{

    "Version": "2012-10-17",
    "Id": "PolicyID",
    "Statement": [
        {
            "Sid": "StmtID",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::accountA-id:role/iam-role-name"
            },
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::bucket-name",
                "arn:aws:s3:::bucket-name/*"
            ]
        }
    ]
}

コンソールから操作。

  1. バケットを選択
  2. アクセス権限
  3. ブロックパブリックアクセス : オフ
  4. バケットポリシーに以下を記入。
    • Principal : 上記で確認した Role の ARN
    • Resource : S3 バケット名
{
    "Version": "2012-10-17",
    "Id": "Policy1589030458582",
    "Statement": [
        {
            "Sid": "Stmt1589030372922",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:role/waf-v2-basepolicy-FirehoseRole-1IZH91LWU3BNE"
            },
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:GetObjectAcl",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::waf-test-cross-account/*",
                "arn:aws:s3:::waf-test-cross-account"
            ]
        }
    ]
}

AWS WAF 側でアラートを発生させて、S3 バケットに保存されていること。

Invalid principal in policy

「アカウントA : バケットポリシー作成」のところでこのエラーに遭遇。

原因は、既にアカウント A で存在しない arn を指定していたため。CloudFormation で作り直した際に arn の値が変わってしまっていた。

アカウント A 側も CloudFormation か Terraform で管理しないと。。

参考

Amazon S3 バケット内のオブジェクトへのクロスアカウントアクセスを提供するには、どうしたらいいですか?

Amazon S3 送信先へのクロスアカウント間の配信

AWSクロスアカウントでS3を読み書きする方法と注意点などを2019年最後にまとめてみた