CORS with CloudFront and S3
The goal of this post is to hopefully save some of you out there hours of ripping your hair out trying to figure out why you get CORS errors on your CloudFront distributuion every once in while.
The intermittent CORS issues ended up coming down to my CloudFront distribution sometimes getting poisoned by a request that was being made made with no Origin
header attached. As an example, while working on setting up
CloudFront I would frequently invalidate the entire cache and then occationally load some of my resources from
CloudFront directly in the browser instead of having a webpage load them via HTML tag or ajax javascript request.
This resulted in CloudFront receiving and caching a response that did not include the appropriate CORS response
headers since no Origin
header on the request means S3 thinks the resource is being loaded directly. This would
then break any cross origin access of the resources. I resolved this by forcing CloudFront to always send a specific Origin
header to S3 which causes S3 to always believe it needs to attach the CORS headers to the response. This prevents blank Origin
header based cache poisoning. The other option is to forward the Origin
header through to S3 and cache based on that. If you go that route you’ll want to make sure you also
forward the Access-Control-Request-Headers
and Access-Control-Request-Method
.
Here’s an example of how I configured the custom headers in CloudFormation to force sending a static Origin to S3:
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
DistCert:
Type: String
Description: The ARN of the certificate uploaded to ACM in us-east-1
DistDomain:
Type: String
Description: The Domain for the CloudFront Distribution
Resources:
AssetBucket:
Type: AWS::S3::Bucket
Properties:
CorsConfiguration:
CorsRules:
-
AllowedHeaders:
- '*'
AllowedMethods:
- GET
- HEAD
AllowedOrigins:
- '*'
AssetBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Id: AssetBucketOriginPolicy
Version: "2012-10-17"
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal:
CanonicalUser: !GetAtt AssetBucketOriginOAI.S3CanonicalUserId
Action: "s3:GetObject"
Resource: !Sub "arn:aws:s3:::${AssetBucket}/*"
Bucket: !Ref AssetBucket
AssetBucketOriginOAI:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: "AssetBucketOriginOAI"
MainDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: The distribution that serves our assets
Enabled: true
PriceClass: PriceClass_All
HttpVersion: http2
DefaultRootObject: index.html
Aliases:
- !Ref DistDomain
ViewerCertificate:
AcmCertificateArn: !Ref DistCert
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1.2_2018
Origins:
- DomainName: !Sub "${AssetBucket}.s3.amazonaws.com"
Id: AssetBucketOrigin
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${AssetBucketOriginOAI}"
# The custom origin header means S3 believes every request comes from the same origin
# which allows us to share the browser cached object between sites while disabling
# origin header forwarding and still getting the proper CORS headers added to the
# response.
OriginCustomHeaders:
-
HeaderName: Origin # Here's where we set a static Origin
HeaderValue: CloudFront
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
- OPTIONS
Compress: true
TargetOriginId: AssetBucketOrigin
ForwardedValues:
QueryString: false # Many real APIs need this to be true
Cookies:
Forward: none
Headers:
- Access-Control-Request-Headers
- Access-Control-Request-Method
# - Origin
ViewerProtocolPolicy: redirect-to-https