I am trying to deploy a static website to S3, and serve it up via Cloudfront. I am using serverless to generate the Cloudformation resources. Once the resources are created, my build process (in CodeBuild and CodePipeline), runs the npm install, npm run build and transfers the build dir to s3 to serve the content. For context, I am using react, specifically create-react-app. After deploying, all resources are created as expected, but the Cloudfront endpoint returns the following "Access Denied" exception, which I am familiar with in S3:
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId><requestId></RequestId>
<HostId><hostId>
</HostId>
</Error>
This makes me believe its the S3 bucket policy, which I have set to be have a Principal value of the Origin Access Identity attached to the CDN. So I'm a little unsure what is missing here to get this working. Any help is appreciated. Below is my info.
Update: The index.html is accessible but the static files are not. I've added the bucket policy below, and here is the file structure of the bucket:
asset-manifest.json
manifest.json
index.html
static/
Cloudformation template generated from serverless deployment:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "The AWS CloudFormation template for this Serverless application",
"Website": {
"Type": "AWS::S3::Bucket",
"Properties": {
"WebsiteConfiguration": {
"ErrorDocument": "index.html",
"IndexDocument": "index.html"
}
}
},
"CloudFrontOriginAccessIdentity": {
"Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
"Properties": {
"CloudFrontOriginAccessIdentityConfig": {
"Comment": "Access Identity for cdn"
}
}
},
"ReadPolicy": {
"Type": "AWS::S3::BucketPolicy",
"Properties": {
"Bucket": {
"Ref": "Website"
},
"PolicyDocument": {
"Statement": [
{
"Action": "s3:GetObject",
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:aws:s3:::",
{
"Ref": "Website"
},
"/*"
]
]
},
"Principal": {
"CanonicalUser": {
"Fn::GetAtt": [
"CloudFrontOriginAccessIdentity",
"S3CanonicalUserId"
]
}
}
}
]
}
}
},
"Distribution": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"Origins": [
{
"DomainName": {
"Fn::GetAtt": [
"Website",
"DomainName"
]
},
"Id": {
"Ref": "Website"
},
"S3OriginConfig": {
"OriginAccessIdentity": {
"Fn::Join": [
"",
[
"origin-access-identity/cloudfront/",
{
"Ref": "CloudFrontOriginAccessIdentity"
}
]
]
}
}
}
],
"Enabled": true,
"HttpVersion": "http2",
"DefaultRootObject": "index.html",
"CustomErrorResponses": [
{
"ErrorCode": 404,
"ResponseCode": 200,
"ResponsePagePath": "/index.html"
}
],
"DefaultCacheBehavior": {
"AllowedMethods": [
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT"
],
"DefaultTTL": 3600,
"ForwardedValues": {
"QueryString": true,
"Cookies": {
"Forward": "none"
}
},
"TargetOriginId": {
"Ref": "Website"
},
"ViewerProtocolPolicy": "redirect-to-https"
},
"ViewerCertificate": {
"CloudFrontDefaultCertificate": "true"
}
}
}
},
},
"Outputs": {
"ServerlessDeploymentBucketName": {
"Value": {
"Ref": "ServerlessDeploymentBucket"
}
},
"WebsiteUrl": {
"Value": {
"Fn::GetAtt": [
"Website",
"WebsiteURL"
]
}
},
"WebSiteBucket": {
"Value": {
"Ref": "Website"
}
}
}
}
S3 bucket policy generated:
{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity {ID for the OAI}"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::{BUCKETNAME}/*"
}
]
}
UPDATED
S3 Bucket YAML file:
Resources:
Website:
Type: AWS::S3::Bucket
Properties:
WebsiteConfiguration:
ErrorDocument: index.html
IndexDocument: index.html
Outputs:
WebsiteUrl:
Value: !GetAtt Website.WebsiteURL
WebSiteBucket:
Value: !Ref Website
Cloudfront YAML file:
Resources:
CloudFrontOriginAccessIdentity:
Type: 'AWS::CloudFront::CloudFrontOriginAccessIdentity'
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: Access Identity for cdn
ReadPolicy:
Type: 'AWS::S3::BucketPolicy'
Properties:
Bucket: !Ref Website
PolicyDocument:
Statement:
- Action: 's3:GetObject'
Effect: Allow
Resource:
!Join [ '', [ 'arn:aws:s3:::', !Ref Website, '/*' ] ]
Principal:
CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
-
# Use the Website as the origin
DomainName: !GetAtt 'Website.DomainName'
Id: !Ref Website
S3OriginConfig:
OriginAccessIdentity: !Join [ '', [ 'origin-access-identity/cloudfront/', !Ref CloudFrontOriginAccessIdentity] ]
Enabled: true
HttpVersion: http2
DefaultRootObject: index.html
# Since React takes care of our routing, we need to make sure every path is served via index.html
# Configure the CDN cache
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html
DefaultCacheBehavior:
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
DefaultTTL: 3600
ForwardedValues:
QueryString: true
Cookies:
Forward: none
# The origin id defined above
TargetOriginId: !Ref Website
ViewerProtocolPolicy: "redirect-to-https" # we want to force https
# The certificate to use when using https
ViewerCertificate:
CloudFrontDefaultCertificate: 'true'
Bucket Policy:
{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity {Id}"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::{bucket-name}/*"
}
]
}