CloudFrontでオリジンリクエストポリシーを設定する時の注意点

Pocket

こんにちは、DX推進室の安齋です。

今回はAWSサーバーレスリソース構築でハマった・工夫した箇所の紹介の第4回目となります。

第4回はCloudFrontでオリジンにS3とAPI Gatewayを設定する際のオリジンリクエストポリシーの注意点を紹介します。

両オリジンを設定する構成はサーバーレスでは良くあるのですが、CloudFormationのみならず、GUIにおいても罠があったので、まとめます。

今回のテーマ

serverless-4-1

上記構成図の赤枠内のCloudFrontのオリジンに、フロントエンドコンテンツのS3と一般用API Gatewayを設定し、CloudFrontにどのようなリクエストが来たらオリジンにリクエストを転送するか、をそれぞれ設定します。API GatewayはCognitoオーソライザーを使用しているため、Authorizationリクエストヘッダが必要になります。

S3のオリジンリクエストポリシーはどうなっていたか

当初、S3のオリジンリクエストポリシーはマネージドポリシーの Managed-AllViewer を設定していました。本ポリシーはリクエストに含まれる全ヘッダ、全Cookie、全クエリストリングをCloudFrontで書き換えることなくオリジンに転送する、という設定です。この設定だと、署名エラーがS3で発生します。

<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>

...省略

<HostId>xxxx</HostId>
</Error>

なぜ署名エラーが起きたか

署名エラーに HostID があることから、Hostヘッダ等に署名がされています。CloudFrontはデフォルトでHostヘッダを書き換えてオリジンにリクエストを転送しますが、本ポリシーにより書き換えが行われなかったこともあり、署名エラーが発生しました。

S3のオリジンリクエストポリシーを外して解決

本ポリシーを外し、オリジンリクエストポリシーを未設定にして解決しました。今回のポイントは DistributionDefaultCacheBehaviorOriginRequestPolicyId を設定しないことです。

CloudFormationテンプレート

今回は、CloudFront OAI、CloudFrontディストリビューション(S3に関する記述のみ)、フロントエンドコンテンツのS3の箇所を抜粋しています。

Resources:
#===============================
# CloudFrontS3OriginAccessIdentity
#===============================
  CloudFrontS3OriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
          Comment: !Sub ${ServiceName}-${Env}-frontend-contents-oai

#===============================
# CloudFrontDistribution
#===============================
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    UpdateReplacePolicy: Retain
    Properties:
      DistributionConfig: 
        Comment: !Sub ${ServiceName}-${Env}-distribution
        DefaultCacheBehavior:
          ViewerProtocolPolicy: redirect-to-https
          TargetOriginId: !Sub S3-${ServiceName}-${Env}-frontend-contents
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
          Compress: false
          SmoothStreaming: false
        DefaultRootObject: index.html
        Enabled: true
        HttpVersion: http2
        IPV6Enabled: true
        Logging: 
          Bucket: <S3 ドメイン>
          IncludeCookies: true
        Origins:
          - Id: !Sub S3-${ServiceName}-${Env}-frontend-contents
            DomainName: !GetAtt FrontendContentsBucket.DomainName
            ConnectionAttempts: 3
            ConnectionTimeout: 10
            S3OriginConfig:
              OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontS3OriginAccessIdentity}
        PriceClass: PriceClass_200
        WebACLId: !Sub arn:aws:wafv2:us-east-1:<アカウントID>:global/webacl/${ServiceName}-${Env}-cloudfront-user-webacl/<リソースId>
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
      Tags:
        - Key: Name
          Value: !Sub ${ServiceName}-${Env}-distribution
    DependsOn:
      - CloudFrontS3OriginAccessIdentity

#===============================
# Frontend Contents S3
#===============================
  FrontendContentsBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Sub ${ServiceName}-${Env}-frontend-contents
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

#===============================
# Frontend Contents S3 Policy
#===============================
  FrontendContentsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    DependsOn: FrontendContentsBucket
    Properties:
      Bucket: !Sub ${ServiceName}-${Env}-frontend-contents
      PolicyDocument:
        Statement:
        - Sid: Allow CloudFront OAI
          Effect: Allow
          Principal:
            AWS:
              !Join
                - " "
                - - "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity"
                  - !Ref CloudFrontS3OriginAccessIdentity
          Action: s3:GetObject
          Resource: !Sub arn:aws:s3:::${ServiceName}-${Env}-frontend-contents/*
        - Sid: Access-to-CloudFront-and-IAMUser
          Effect: Deny
          Principal: '*'
          Action: s3:*
          Resource:
            - !Sub arn:aws:s3:::${ServiceName}-${Env}-frontend-contents
            - !Sub arn:aws:s3:::${ServiceName}-${Env}-frontend-contents/*
          Condition:
            StringNotLike:
              aws:userId: <IAMロールID>:*
            ArnNotEquals:
              aws:PrincipalArn:
                !Join
                  - " "
                  - - "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity"
                    - !Ref CloudFrontS3OriginAccessIdentity
        - Sid: Access-to-SecureTransport
          Effect: Deny
          Principal: '*'
          Action: s3:*
          Resource:
            - !Sub arn:aws:s3:::${ServiceName}-${Env}-frontend-contents
            - !Sub arn:aws:s3:::${ServiceName}-${Env}-frontend-contents/*
          Condition:
            Bool:
              aws:SecureTransport: false

参考資料

  • 管理オリジンリクエストポリシーについて
    • https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policies-list
  • HTTP リクエストヘッダーと CloudFront の動作 (カスタムオリジンおよび S3 オリジン)
    • https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html#request-custom-headers-behavior
  • CloudFront and S3: SignatureDoesNotMatch, the request signature we calculated does not match the signature you provided
    • https://www.codejam.info/2021/02/cloudfront-s3-signature-does-not-match.html
  • AWS::CloudFront::Distribution DefaultCacheBehavior
    • https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-defaultcachebehavior.html

API Gatewayのオリジンリクエストポリシーはどうなっていたか

当初、API Gatewayのオリジンリクエストポリシーは、S3同様にマネージドポリシーの Managed-AllViewer を設定していました。この設定だと、HostヘッダがCloudFrontのままであるため、API Gatewayから403エラーが返されました。

HTTP/2 403
content-type: application/json
content-length: 23
date: Tue, 27 Jul 2021 10:51:22 GMT
x-amzn-requestid: xxxxxx
x-amzn-errortype: ForbiddenException
x-amz-apigw-id: yyyyyyy
x-cache: Error from cloudfront
via: 1.1 xxxxxxxyyyyyyyzzzzzz.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-C2
x-amz-cf-id: xxxxxxxxxxxxxxxxxxxxxxxx

API GatewayにHostヘッダを転送しないオリジンリクエストポリシーを作ろうとした

Hostヘッダは不要で、Authorizationヘッダが必要なオリジンリクエストポリシーを作ろうとしました。しかし、Authorizationヘッダをオリジンリクエストポリシーに含めることができませんでした。

注意: オリジンリクエストポリシーを使用して認証ヘッダーを転送することはできません。キャッシュが不正なリクエストを満たすのを防ぐために、ヘッダーはキャッシュキーの一部である必要があります。認証ヘッダーを転送するオリジンリクエストポリシーを作成しようとすると、CloudFront は HTTP 400 エラーを返します。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudfront-authorization-header/

GUIからキャッシュのレガシー設定を行って解決

そのため、オリジンリクエストポリシーを使用せず、CloudFrontディストリビューションのレガシー設定を使用し、Include the following hedearsAuthorizationReferer を設定して解決しました。ただ、GUIには同設定が提供されていますが、CloudFormationには同設定のリソースが提供されていませんので注意です。

serverless-4-2

参考資料

  • CloudFront でオリジンに Authorization ヘッダを送る設定をしようと思うとエラーが出て設定できない
    • https://qiita.com/t-kigi/items/6ea9d2656c49b8e40af4

SAMテンプレート

一般用API GatewayとLambdaの箇所を抜粋します。

#===============================
# Globals Selection
#===============================
Globals:
#===============================
# API Gateway
#===============================  
  Api:
    CacheClusterEnabled: false
    EndpointConfiguration:
      Type: EDGE
    MethodSettings:
      - CachingEnabled: false
        DataTraceEnabled: true
        HttpMethod: '*'
        LoggingLevel: INFO
        MetricsEnabled: true
        ResourcePath: '/*'
        ThrottlingBurstLimit: 1000
        ThrottlingRateLimit: 1000
    TracingEnabled: true

#===============================
# Lambda
#===============================
  Function:
    Runtime: go1.x
    Timeout: 27
    VpcConfig:
      SubnetIds:
        - <サブネットID>
        - <サブネットID>
      SecurityGroupIds:
        - <セキュリティグループID>
    EventInvokeConfig:
      MaximumRetryAttempts: 0
    KmsKeyArn: <KMS ARN>
    MemorySize: 256
    Tracing: Active

Resources:
#===============================
# API Gateway for General 
#===============================
  GeneralApiResource:
    Type: AWS::Serverless::Api
    Properties:
      Auth:
        AddDefaultAuthorizerToCorsPreflight: false
        ApiKeyRequired: false
        Authorizers:
          CognitoAuth:
            UserPoolArn: <CognitoユーザープールARN>
            Identity:
              ReauthorizeEvery: 0
        DefaultAuthorizer: CognitoAuth
      Name: !Sub ${ServiceName}-${Env}-general
      StageName: api

#===============================
# Lambda for General
#===============================
  PostTech4All:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${ServiceName}-${Env}-post_tech4all
      CodeUri: src/post_tech4all/
      Handler: post_tech4all
      Role: !ImportValue LambdaBackendRoleArn
      Events:
        CatchAll:
          Type: Api
          Properties:
            Path: /tech4all
            Method: POST
            RestApiId: !Ref GeneralApiResource

#==============================
# Outputs
#==============================
Outputs:
#=======================================
# API Gateway DomainName
#=======================================
  GeneralApiResourceDomainName:
    Value: !Sub ${GeneralApiResource}.execute-api.${AWS::Region}.amazonaws.com
    Export:
      Name: GeneralApiResourceDomainName

CloudFormationテンプレート

先に掲載した、CloudFrontディストリビューションのAPI Gatewayに関する箇所を抜粋しています。

#===============================
# CloudFrontDistribution
#===============================
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    UpdateReplacePolicy: Retain
    Properties:
      DistributionConfig: 
        Comment: !Sub ${ServiceName}-${Env}-distribution
# ★★★ココからAPI Gatewayの記述★★★
        CacheBehaviors:
          - PathPattern: /api/*
            ViewerProtocolPolicy: https-only
            TargetOriginId: !Sub APIGateway-${ServiceName}-${Env}
            AllowedMethods:
              - GET
              - HEAD
              - OPTIONS
              - PUT
              - PATCH
              - POST
              - DELETE
            CachedMethods:
              - GET
              - HEAD
            CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
            Compress: false
            SmoothStreaming: false
# ★★★ココまでAPI Gatewayの記述★★★
        DefaultCacheBehavior:
...省略
        Origins:
# ★★★ココからAPI Gatewayの記述★★★
          - Id: !Sub APIGateway-${ServiceName}-${Env}
            DomainName: !ImportValue GeneralApiResourceDomainName
            ConnectionAttempts: 3
            ConnectionTimeout: 10
            CustomOriginConfig:
              HTTPSPort: 443
              OriginKeepaliveTimeout: 5
              OriginProtocolPolicy: https-only
              OriginReadTimeout: 30
              OriginSSLProtocols:
                - TLSv1.2
            OriginCustomHeaders:
              - HeaderName: x-origin-key
                HeaderValue: xxxxxxxxxxxxxxxx
# ★★★ココまでAPI Gatewayの記述★★★
          - Id: !Sub S3-${ServiceName}-${Env}-frontend-contents
...省略

めでたしめでたし

無事にCloudFront経由でS3とAPI Gatewayにリクエストを送ることができました。Cognitoオーソライザーを使用しているAPI GatewayをCloudFrontから配信する際は、オリジンリクエストポリシーを設定できないことに注意です。

ちなみに、一般用API GatewayをCloudFrontから配信した理由は、IP制限がされていないS3のフロントエンドコンテンツと同API Gatewayのセキュリティを高めるため、有料のWAFマネージドルールを利用したく、インターネットアクセスされる窓口をCloudFront1つに集約させたいからでした。

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です