CloudFront+S3 OACの設定をCloudFormationでやってみた

2022.09.30

静的コンテンツを配信するためにCloudFrontとS3を利用した際、OACを利用したのでブログに残しておきます。

OACとは

CloudFrontのオリジンにS3を指定する際、S3へのアクセスをCloudFrontからだけに制限するための設定です。
S3へのアクセス制限はOAIという機能でも可能ですがOACでは以下のものに対応しています。
Amazon S3 オリジンへのアクセスの制限

AWS KMS による Amazon S3 サーバー側の暗号化 (SSE-KMS)
Amazon S3 に対する動的なリクエスト (POST、PUT など)

詳しくは以下のブログとドキュメントをご確認ください。

作成したCloudFormationテンプレート

以下のテンプレートをデプロイするとCloudFrontとS3が作成されます。
今回はS3とCloudFrontでテンプレートを分割しました。
デプロイをする際は順番があるのですぐに試したい場合はデプロイに飛んでください。

S3のテンプレート

S3バケットはKMS暗号化を使用して作成しました。
AWS::KMS::Key
AWS::S3::Bucket

AWSTemplateFormatVersion: "2010-09-09"

Description: S3&KMS Stack

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  BucketName:
    Type: String

  IAMUserARN:
    Type: String

Resources:
# ------------------------------------------------------------#
# KMS
# ------------------------------------------------------------# 
  KMS:
    Type: AWS::KMS::Key
    Properties: 
      Description: OAC Test
      Enabled: true
      KeyPolicy: 
        Version: '2012-10-17'
        Statement: 
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal: 
              AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
            Action: 'kms:*'
            Resource: '*'
          - Sid: 'Allow access for Key Administrators'
            Effect: Allow
            Principal: 
              AWS: !Ref IAMUserARN
            Action: 
              - 'kms:Create*'
              - 'kms:Describe*'
              - 'kms:Enable*'
              - 'kms:List*'
              - 'kms:Put*'
              - 'kms:Update*'
              - 'kms:Revoke*'
              - 'kms:Disable*'
              - 'kms:Get*'
              - 'kms:Delete*'
              - 'kms:TagResource'
              - 'kms:UntagResource'
              - 'kms:ScheduleKeyDeletion'
              - 'kms:CancelKeyDeletion'
              - 'kms:CreateGrant'
              - 'kms:ListGrants'
              - 'kms:RevokeGrant'
            Resource: '*'
          - Sid: 'Allow use of the key'
            Effect: Allow
            Principal: 
              AWS: !Ref IAMUserARN
            Action: 
              - 'kms:Encrypt'
              - 'kms:Decrypt'
              - 'kms:ReEncrypt*'
              - 'kms:GenerateDataKey*'
              - 'kms:DescribeKey'
            Resource: '*'
          - Sid: 'Allow use of the key'
            Effect: Allow
            Principal: 
              Service: 
                - cloudfront.amazonaws.com
            Action: 
              - 'kms:Decrypt'
              - 'kms:Encrypt'
              - 'kms:GenerateDataKey*'
            Resource: '*'
            Condition: 
              StringEquals:
                aws:SourceArn:
                  - !Join 
                    - ''
                    - - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/'
                      - !ImportValue CloudFrontID
      PendingWindowInDays: 7

# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------# 
  S3:
    Type: AWS::S3::Bucket
    Properties: 
      BucketEncryption: 
        ServerSideEncryptionConfiguration: 
          - BucketKeyEnabled: true
            ServerSideEncryptionByDefault:
              KMSMasterKeyID: !Ref KMS
              SSEAlgorithm: aws:kms
      BucketName: !Ref BucketName
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties: 
      Bucket: !Ref S3
      PolicyDocument: 
        Version: "2008-10-17"
        Statement: 
          - Sid: "AllowCloudFrontServicePrincipal"
            Effect: "Allow"
            Principal: 
              Service: 
                - "cloudfront.amazonaws.com"
            Action: 
              - "s3:GetObject"
            Resource: 
              - !Sub ${S3.Arn}/*
            Condition: 
              StringEquals:
                AWS:SourceArn: 
                  - !Join 
                    - ''
                    - - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/'
                      - !ImportValue CloudFrontID

Outputs:
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------# 
  S3:
    Value: !GetAtt S3.DomainName
    Export: 
      Name: S3

CloudFrontのテンプレート

CloudFrontでS3をオリジンにする際は「CustomOriginConfig」(静的Webサイトホスティングを設定している場合) もしくは「S3OriginConfig」の設定が必要です。
今回は静的Webサイトホスティングではないので「S3OriginConfig」を設定しています。
また「S3OriginConfig」の中にある「OriginAccessIdentity」は空の要素を指定してオリジン アクセスIDを利用しない状態を作っています。
OAIは使用しませんが、記載しないとエラーになります。
AWS::CloudFront::Distribution
AWS::CloudFront::OriginAccessControl

AWSTemplateFormatVersion: "2010-09-09"

Description: CloudFront Stack

Resources:
# ------------------------------------------------------------#
# CloudFront
# ------------------------------------------------------------# 
  OAC: 
    Type: AWS::CloudFront::OriginAccessControl
    Properties: 
      OriginAccessControlConfig:
        Description: Access Control
        Name: OAC
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  CloudFront:
    Type: AWS::CloudFront::Distribution
    Properties: 
      DistributionConfig:
        DefaultCacheBehavior: 
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
          TargetOriginId: S3
          ViewerProtocolPolicy: allow-all
        Enabled: true
        Origins: 
          - DomainName: !ImportValue S3
            Id: S3
            OriginAccessControlId: !GetAtt OAC.Id
            S3OriginConfig: 
              OriginAccessIdentity: ''
        PriceClass: PriceClass_200

Outputs:
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------# 
  CloudFrontID:
    Value: !Ref CloudFront
    Export: 
      Name: CloudFrontID

デプロイ

デプロイはAWS CLIを使用して行いました。
まずはS3をデプロイします。
S3を最初にデプロイする際は以下のようにKMSキーポリシーのCloudFrontに関係する部分とS3バケットポリシーをコメントへ変更しておきます。
理由としてはCloudFrontのARNを指定しているためコメントにしていない場合、作成時にエラーが発生するためです。

AWSTemplateFormatVersion: "2010-09-09"

Description: S3&KMS Stack

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  BucketName:
    Type: String

  IAMUserARN:
    Type: String

Resources:
# ------------------------------------------------------------#
# KMS
# ------------------------------------------------------------# 
  KMS:
    Type: AWS::KMS::Key
    Properties: 
      Description: OAC Test
      Enabled: true
      KeyPolicy: 
        Version: '2012-10-17'
        Statement: 
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal: 
              AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
            Action: 'kms:*'
            Resource: '*'
          - Sid: 'Allow access for Key Administrators'
            Effect: Allow
            Principal: 
              AWS: !Ref IAMUserARN
            Action: 
              - 'kms:Create*'
              - 'kms:Describe*'
              - 'kms:Enable*'
              - 'kms:List*'
              - 'kms:Put*'
              - 'kms:Update*'
              - 'kms:Revoke*'
              - 'kms:Disable*'
              - 'kms:Get*'
              - 'kms:Delete*'
              - 'kms:TagResource'
              - 'kms:UntagResource'
              - 'kms:ScheduleKeyDeletion'
              - 'kms:CancelKeyDeletion'
              - 'kms:CreateGrant'
              - 'kms:ListGrants'
              - 'kms:RevokeGrant'
            Resource: '*'
          - Sid: 'Allow use of the key'
            Effect: Allow
            Principal: 
              AWS: !Ref IAMUserARN
            Action: 
              - 'kms:Encrypt'
              - 'kms:Decrypt'
              - 'kms:ReEncrypt*'
              - 'kms:GenerateDataKey*'
              - 'kms:DescribeKey'
            Resource: '*'
#          - Sid: 'Allow use of the key'
#            Effect: Allow
#            Principal: 
#              Service: 
#                - cloudfront.amazonaws.com
#            Action: 
#              - 'kms:Decrypt'
#              - 'kms:Encrypt'
#              - 'kms:GenerateDataKey*'
#            Resource: '*'
#            Condition: 
#              StringEquals:
#                aws:SourceArn:
#                  - !Join 
#                    - ''
#                    - - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/'
#                      - !ImportValue CloudFrontID
      PendingWindowInDays: 7

# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------# 
  S3:
    Type: AWS::S3::Bucket
    Properties: 
      BucketEncryption: 
        ServerSideEncryptionConfiguration: 
          - BucketKeyEnabled: true
            ServerSideEncryptionByDefault:
              KMSMasterKeyID: !Ref KMS
              SSEAlgorithm: aws:kms
      BucketName: !Ref BucketName
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

#  S3BucketPolicy:
#    Type: AWS::S3::BucketPolicy
#    Properties: 
#      Bucket: !Ref S3
#      PolicyDocument: 
#        Version: "2008-10-17"
#        Statement: 
#          - Sid: "AllowCloudFrontServicePrincipal"
#            Effect: "Allow"
#            Principal: 
#              Service: 
#                - "cloudfront.amazonaws.com"
#            Action: 
#              - "s3:GetObject"
#            Resource: 
#              - !Sub ${S3.Arn}/*
#            Condition: 
#              StringEquals:
#                AWS:SourceArn: 
#                  - !Join 
#                    - ''
#                    - - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/'
#                      - !ImportValue CloudFrontID

Outputs:
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------# 
  S3:
    Value: !GetAtt S3.DomainName
    Export: 
      Name: S3

1. S3作成

以下のコマンドを実行します。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=BucketName,ParameterValue=S3バケット名 ParameterKey=IAMUserARN,ParameterValue=IAMユーザのARN

2. CloudFront作成

S3のスタックが完成したらCloudFrontを作成していきます。
CloudFrontのテンプレートは編集せずに以下のコマンドを実行します。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名

3. S3バケットポリシーとKMSポリシー追加

CloudFrontの作成が完了したらコメントに変更していたS3バケットポリシーとKMSポリシーの部分を戻して以下のコマンドを実行します。

aws cloudformation create-change-set --change-set-name 変更セット名 --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=BucketName,ParameterValue=S3バケット名 ParameterKey=IAMUserARN,ParameterValue=IAMユーザのARN
aws cloudformation execute-change-set --change-set-name 変更セットのARN

4. 動作確認

以下のHTMLファイルをS3にアップロードします。

<html>
<head>
<meta charset="UTF-8">
<title>テストページ</title>
</head>
<body bgcolor="#10100E" text="#cccccc">
小林 陸<br/>
</body>
</html>

S3へのアップロードは以下のコマンドを実行します。
test.htmlはアップロードするHTMLファイル名になります。

aws s3 cp ./test.html s3://バケット名

ファイルアップロード後CloudFrontのドメイン名でブラウザからアクセスすると以下のようにページが表示されます。

オブジェクトURLに直接アクセスしても表示されないことも確認できます。

テンプレート説明

今回作成したCloudFormationテンプレートで重要な部分を説明します。
CloudFrontのテンプレートでは9行目から記載しているOACを作成している部分と36行目のOACを指定している部分が今回メインになります。
OACの作成は「AWS::CloudFront::OriginAccessControl」というものを使用して作成します。
各パラメータでOACの名前、オリジンのタイプ、オリジンへのリクエストに署名をする設定、署名方法を設定しています。
少し内容がずれますが、普段AWSのAPIを利用するときは裏側でSigV4というもので署名を行いAPIリクエスト実行者の認証、承認を行っています。
この機能を使ってCloudFrontからS3オリジンへのアクセスに認証、承認、拒否を行いアクセスを制御しています。
署名についての詳細は以下のドキュメントをご確認ください。
署名バージョン 4 の署名プロセス Authenticating Requests (AWS Signature Version 4)

# OAC作成部分
  OAC: 
    Type: AWS::CloudFront::OriginAccessControl
    Properties: 
      OriginAccessControlConfig:
        Description: Access Control
        Name: OAC
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

# OAC指定部分
            OriginAccessControlId: !GetAtt OAC.Id

S3のテンプレートではKMSキーポリシーとS3バケットポリシーが重要な部分になっています。
キーポリシーではCloudFrontのプリンシパルを指定してKMSにアクセスできるようにしています。
S3バケットポリシーも同様にCloudFrontからアクセスできるようにプリンシパルで指定しています。

# キーポリシー抜粋
          - Sid: 'Allow use of the key'
            Effect: Allow
            Principal: 
              Service: 
                - cloudfront.amazonaws.com
            Action: 
              - 'kms:Decrypt'
              - 'kms:Encrypt'
              - 'kms:GenerateDataKey*'
            Resource: '*'
            Condition: 
              StringEquals:
                aws:SourceArn:
                  - !Join 
                    - ''
                    - - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/'
                      - !ImportValue CloudFrontID

# S3バケットポリシー
          - Sid: "AllowCloudFrontServicePrincipal"
            Effect: "Allow"
            Principal: 
              Service: 
                - "cloudfront.amazonaws.com"
            Action: 
              - "s3:GetObject"
            Resource: 
              - !Sub ${S3.Arn}/*
            Condition: 
              StringEquals:
                AWS:SourceArn: 
                  - !Join 
                    - ''
                    - - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/'
                      - !ImportValue CloudFrontID

さいごに

今回はCloudFrontのOACをCloudFormationで作成してみました。
CloudFormationではなくマネジメントコンソールからの作成の場合、バケットポリシーをコピーすることが可能なので簡単に設定することができます。
以下のドキュメントにOAIからOACへ移行する際のサンプルバケットポリシーが記載されていますので移行を検討の際はご確認ください。