Cloudfront give Access denied response created through AWS CDK Python for S3 bucket origin without public Access

Question:

Created Cloud Front web distribution with AWS CDK for S3 bucket without public access.
Able to create Origin access identity, and deploy but on successful deploy i get access denied response on browser.

Grant Read Permissions on Bucket from Origin settings will be set to No, setting this to Yes manually everything will work fine, but this setting needs to be achieved through AWS CDK and python.
Below is my code.

from aws_cdk import aws_cloudfront as front, aws_s3 as s3

class CloudFrontStack(core.Stack):        
    def __init__(self, scope: core.Construct, idx: str, **kwargs) -> None:
        super().__init__(scope, idx, **kwargs)

        bucket = s3.Bucket.from_bucket_name(self, 'CloudFront',bucket_name="bucket_name")

        oia = aws_cloudfront.OriginAccessIdentity(self, 'OIA', comment="Created By CDK")
        bucket.grant_read(oia)

        s3_origin_source = aws_cloudfront.S3OriginConfig(s3_bucket_source=bucket, origin_access_identity=oia)

        source_config = aws_cloudfront.SourceConfiguration(s3_origin_source=s3_origin_source,
                                                           origin_path="bucket_path",
                                                           behaviors=[aws_cloudfront.Behavior(is_default_behavior=True)])

        aws_cloudfront.CloudFrontWebDistribution(self, "cloud_front_name",
                                                 origin_configs=[source_config],
                                                 comment='Cloud Formation created',
                                                 default_root_object='index.html')

I also tried adding the permissions to the as below but still no luck.

policyStatement = aws_iam.PolicyStatement()
policyStatement.add_resources()

policyStatement.add_actions('s3:GetBucket*');
policyStatement.add_actions('s3:GetObject*');
policyStatement.add_actions('s3:List*');
policyStatement.add_resources(bucket.bucket_arn);
policyStatement.add_canonical_user_principal(oia.cloud_front_origin_access_identity_s3_canonical_user_id);
code_bucket.add_to_resource_policy(policyStatement);
Asked By: santosh

||

Answers:

I tried to mimic this and was able to integrate Cloudfront distribution to a private S3 bucket successfully. However, I used TS for my stack. I am sure it will be easy to correlate below code to Python version. Assume there is an index.html file in dist

aws-cdk v1.31.0 (latest as of March 29th, 2020)

import { App, Stack, StackProps } from '@aws-cdk/core';
import { BucketDeployment, Source } from '@aws-cdk/aws-s3-deployment';
import { CloudFrontWebDistribution, OriginAccessIdentity } from '@aws-cdk/aws-cloudfront';
import { BlockPublicAccess, Bucket, BucketEncryption } from '@aws-cdk/aws-s3';

export class HelloCdkStack extends Stack {
  constructor(scope: App, id: string, props?: StackProps) {
    super(scope, id, props);

    const myFirstBucket = new Bucket(this, 'MyFirstBucket', {
      versioned: true,
      encryption: BucketEncryption.S3_MANAGED,
      bucketName: 'cdk-example-bucket-for-test',
      websiteIndexDocument: 'index.html',
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL
    });

    new BucketDeployment(this, 'DeployWebsite', {
      sources: [Source.asset('dist')],
      destinationBucket: myFirstBucket
    });

    const oia = new OriginAccessIdentity(this, 'OIA', {
      comment: "Created by CDK"
    });
    myFirstBucket.grantRead(oia);

    new CloudFrontWebDistribution(this, 'cdk-example-distribution', {
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: myFirstBucket,
            originAccessIdentity: oia
          },
          behaviors: [
            { isDefaultBehavior: true }
          ]
        }
      ]
    });
  }
}

== Update == [S3 bucket without Web Hosting]

Here is an example where S3 is used as an Origin without Web hosting. It works as expected.

import { App, Stack, StackProps } from '@aws-cdk/core';
import { BucketDeployment, Source } from '@aws-cdk/aws-s3-deployment';
import { CloudFrontWebDistribution, OriginAccessIdentity } from '@aws-cdk/aws-cloudfront';
import { BlockPublicAccess, Bucket, BucketEncryption } from '@aws-cdk/aws-s3';

export class CloudfrontS3Stack extends Stack {
  constructor(scope: App, id: string, props?: StackProps) {
    super(scope, id, props);

    // Create bucket (which is not a static website host), encrypted AES-256 and block all public access
    // Only Cloudfront access to S3 bucket
    const testBucket = new Bucket(this, 'TestS3Bucket', {
      encryption: BucketEncryption.S3_MANAGED,
      bucketName: 'cdk-static-asset-dmahapatro',
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL
    });

    // Create Origin Access Identity to be use Canonical User Id in S3 bucket policy
    const originAccessIdentity = new OriginAccessIdentity(this, 'OAI', {
      comment: "Created_by_dmahapatro"
    });
    testBucket.grantRead(originAccessIdentity);

    // Create Cloudfront distribution with S3 as Origin
    const distribution = new CloudFrontWebDistribution(this, 'cdk-example-distribution', {
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: testBucket,
            originAccessIdentity: originAccessIdentity
          },
          behaviors: [
            { isDefaultBehavior: true }
          ]
        }
      ]
    });

    // Upload items in bucket and provide distribution to create invalidations
    new BucketDeployment(this, 'DeployWebsite', {
      sources: [Source.asset('dist')],
      destinationBucket: testBucket,
      distribution,
      distributionPaths: ['/images/*.png']
    });
  }
}

== UPDATE == [S3 Bucket imported instead of creating in the same stack]

When we refer to an existing S3 bucket the issue can be recreated.

Reason:
The root cause of the issue lies here in this line of code. autoCreatePolicy will always be false for an imported S3 bucket.
To make addResourcePolicy work either the imported bucket has to already have an existing Bucket policy so that the new policy statements can be appended or manually create new BucketPolicy and add the policy statements. In the below code I have manually created the bucket policy and add the required policy statements. This is very close to the github issue #941 but the subtle difference is between creating a bucket in the stack vs importing an already created bucket.

import { App, Stack, StackProps } from '@aws-cdk/core';
import { CloudFrontWebDistribution, OriginAccessIdentity } from '@aws-cdk/aws-cloudfront';
import { Bucket, BucketPolicy } from '@aws-cdk/aws-s3';
import { PolicyStatement } from '@aws-cdk/aws-iam';

export class CloudfrontS3Stack extends Stack {
  constructor(scope: App, id: string, props?: StackProps) {
    super(scope, id, props);

    const testBucket = Bucket.fromBucketName(this, 'TestBucket', 'dmahapatro-personal-bucket');

    // Create Origin Access Identity to be use Canonical User Id in S3 bucket policy
    const originAccessIdentity = new OriginAccessIdentity(this, 'OAI', {
      comment: "Created_by_dmahapatro"
    });

    // This does not seem to work if Bucket.fromBucketName is used
    // It works for S3 buckets which are created as part of this stack
    // testBucket.grantRead(originAccessIdentity);

    // Explicitly add Bucket Policy 
    const policyStatement = new PolicyStatement();
    policyStatement.addActions('s3:GetBucket*');
    policyStatement.addActions('s3:GetObject*');
    policyStatement.addActions('s3:List*');
    policyStatement.addResources(testBucket.bucketArn);
    policyStatement.addResources(`${testBucket.bucketArn}/*`);
    policyStatement.addCanonicalUserPrincipal(originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId);

    // testBucket.addToResourcePolicy(policyStatement);

    // Manually create or update bucket policy
    if( !testBucket.policy ) {
      new BucketPolicy(this, 'Policy', { bucket: testBucket }).document.addStatements(policyStatement);
    } else {
      testBucket.policy.document.addStatements(policyStatement);
    }

    // Create Cloudfront distribution with S3 as Origin
    const distribution = new CloudFrontWebDistribution(this, 'cdk-example-distribution', {
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: testBucket,
            originAccessIdentity: originAccessIdentity
          },
          behaviors: [
            { isDefaultBehavior: true }
          ]
        }
      ]
    });
  }
}
Answered By: dmahapatro

Adding onto @dmahapatro answer, the Distribution interface has changed yet again.

As of August 2022, CDK version 2.37.1, the Distribution should now take a
defaultBehavior key of type BehaviorOptions.

The originAccessIdentity is now added to the
S3OriginProps of S3Origin.

Before

new CloudFrontWebDistribution(this, 'cdk-example-distribution', {
  originConfigs: [
    {
      s3OriginSource: {
        s3BucketSource: myFirstBucket,
        originAccessIdentity: oia
      },
      behaviors: [
        { isDefaultBehavior: true }
      ]
    }
  ]
});

After

const distribution = new Distribution(this, "cdk-example-distribution", {
  defaultBehavior: {
    origin: new S3Origin(testBucket, {
      originAccessIdentity: originAccessIdentity,
    }),
  }
});
Answered By: Luke Pafford

Complementing the answers above, I will post a working python solution that allows a private S3 bucket to be accessed by Cloudfront. Three points are essential in order to do this:

  1. Create a Cloudfront Origin Access Identity:

    origin_access_identity = cloudfront.OriginAccessIdentity(
             self,
             'cdkTestOriginAccessIdentity'
    )
    
  2. Grant access to a Cloudfront Origin Access Identity to read from the S3 bucket: self.s3_bucket.grant_read(origin_access_identity) (Note that AWS docs indicate that Origin Access Identity is being deprecated in favor of Origin Access Control, but in CDK, OAI was not implemented yet)

  3. The Origin Access Identity instance must be provided to the Cloudfront Distribution instance to allow Cloudfront requests to be allowed by S3:

    origin=origins.S3Origin(
              self.s3_bucket,
              origin_access_identity=access_identity)
    

The final code is as follows:

   import aws_cdk as cdk
   from aws_cdk import (
     aws_s3 as s3,
     aws_s3_deployment as s3deploy,
     aws_cloudfront as cloudfront,
     aws_cloudfront_origins as origins
   )


    class Presentation(cdk.Stack):
      def __init__(self, scope, id, **kwargs):
        super().__init__(scope, id, **kwargs)

        # Define a S3 bucket that contains a static website
        self.s3_bucket = self._create_hosting_s3_bucket()

        # Creates Cloudfront Origin Access Identity to access a restricted S3
        origin_access_identity = cloudfront.OriginAccessIdentity(
            self,
            'cdkTestOriginAccessIdentity'
        )

        # Allows Origin Access Control to read from S3
        self.s3_bucket.grant_read(origin_access_identity)

        # Define Cloudfront CDN that delivers from S3 bucket
        self.cdn = self._create_cdn(access_identity=origin_access_identity)

      def _create_hosting_s3_bucket(self):
        """ Returns a S3 instance that serves a static website """
        website_bucket = s3.Bucket(
            self,
            'static-website-for-cdkdemo',
            removal_policy=cdk.RemovalPolicy.DESTROY,
            access_control=s3.BucketAccessControl.PRIVATE,
        )

        # Populates bucket with files from local file system
        s3deploy.BucketDeployment(
            self,
            'DeployWebsite',
            destination_bucket=website_bucket,
            sources=[
                s3deploy.Source.asset('../frontend')
            ],
            retain_on_delete=False,
        )
        return website_bucket

      def _create_cdn(self, access_identity):
        """ Returns a CDN that delivers from a S3 bucket """
        return cloudfront.Distribution(
            self,
            'myDist',
            default_behavior=cloudfront.BehaviorOptions(
                origin=origins.S3Origin(
                    self.s3_bucket,
                    origin_access_identity=access_identity,
                )
            )
        )
Answered By: Eduardo Matsuoka