こんにちは、DX推進室の安齋です。
今回はAWSサーバーレスリソース構築でハマった・工夫した箇所の紹介の第4回目となります。
- 第1回は API GatewayでIAM認証を行う
- 第2回は API GatewayのCloudWatchLogsの設定をIaCで行う
- 第3回は Cognitoユーザープールでグループ毎にリソース制御を行う
第4回はCloudFrontでオリジンにS3とAPI Gatewayを設定する際のオリジンリクエストポリシーの注意点を紹介します。
両オリジンを設定する構成はサーバーレスでは良くあるのですが、CloudFormationのみならず、GUIにおいても罠があったので、まとめます。
今回のテーマ
上記構成図の赤枠内の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のオリジンリクエストポリシーを外して解決
本ポリシーを外し、オリジンリクエストポリシーを未設定にして解決しました。今回のポイントは Distribution
の DefaultCacheBehavior
に OriginRequestPolicyId
を設定しないことです。
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 hedears
に Authorization
と Referer
を設定して解決しました。ただ、GUIには同設定が提供されていますが、CloudFormationには同設定のリソースが提供されていませんので注意です。
参考資料
- 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つに集約させたいからでした。