Cloudfront with S3 origin returns AccessDenied when using OAI restricted bucket policy

Viewed 1689

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}/*"
        }
    ]
}
3 Answers

Per @Marcins answer above, that confirmed that everything was fine with the permissions for the index.html but the 403 persisted. After some lengthy investigation, I found this post here, which details the need to add a specific error response for 403 to the Cloudfront Distribution, similar to the one shown for 404. So my new CustomErrorResponse lokks like this:

        CustomErrorResponses:
          - ErrorCode: 404
            ResponseCode: 200
            ResponsePagePath: /index.html
          - ErrorCode: 403
            ResponseCode: 200
            ResponsePagePath: /index.html

Updated answer based on comments.

I tried to replicate the issue and deployed your YAML templates in my sandbox account. I found that they are correct. The sample index.html that I uploaded to the bucket created works as expected and is accessible from CF distro created using url of the form:

https://d1237123arhp6.cloudfront.net/

No issues with CF distro, bucket policy nor OAI were identified. The template works as is without modifications.

Template used:

Resources:

  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: 
            !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'

Update the S3 bucket policy will fix this issue. Along with s3:GetObject use listbucket permission as well then it works as expected. I'm posting bucket policy in json format you can change it to yaml format


{
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity FOO"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-bucket/*"
        },
        {
            "Sid": "2",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity FOO"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::my-bucket"
        }
    ]
}

Related