Deliver S3 content via CloudFront using OAC

Deliver S3 content via CloudFront using OAC

Restricting access to origin with OAC when delivering S3 content from CloudFront

The following page shows how to use OAI to restrict access to origin when delivering S3 bucket content from CloudFront.

https://awstut.com/en/2022/05/16/s3-content-delivery-via-cloudfront-oai-en

OAC (Origin Access Control) was announced in August 2022.
AWS officially recommends using OAC rather than OAI.

We recommend using OAC because it supports:

All Amazon S3 buckets in all AWS Regions, including opt-in Regions launched after December 2022

Amazon S3 server-side encryption with AWS KMS (SSE-KMS)

Dynamic requests (PUT and DELETE) to Amazon S3

Restricting access to an Amazon S3 origin

In this article, we will see how to access S3 buckets via CloudFront using OAC.

Environment

Diagram of delivering S3 content via CloudFront using OAC

The overall configuration is the same as in the opening page.
Create an OAC for CloudFront and configure the S3 bucket policy to allow access from this OAC.

CloudFormation template files

Build the above configuration with CloudFormation.
The CloudFormation templates are located at the following URL

https://github.com/awstut-an-r/awstut-fa/tree/main/096

Explanation of key points of the template files

S3 bucket

Resources:
  Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref Prefix
      AccessControl: Private
Code language: YAML (yaml)

This is the bucket where the contents are placed.
No special configuration is required on the S3 bucket side when using OAC.

OAC

Resources:
  OAC:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: !Ref Prefix
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4
Code language: YAML (yaml)

There are three important parameters for creating an OAC.

The first is the OriginAccessControlOriginType property.
This parameter can only be taken by “s3”.

The second is the SigningBehavior property.
This parameter is related to whether CloudFront signs the origin (S3) when accessing it.
AWS officially mentions the following

We recommend most customers use “Sign requests” option as it ensures your applications will always work because CloudFront will always sign the incoming request. Additionally, by having CloudFront to sign your requests, your applications’ performance is improved as less data is transferred between client and CloudFront.

Amazon CloudFront introduces Origin Access Control (OAC)

Follow the above and set it to “always” so that a signature request is always made.

The third is the SigningProtocol property.
This parameter can only take “sigv4”.

CloudFront

Resources:
  Distribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          Compress: true
          ForwardedValues:
            Cookies:
              Forward: none
            QueryString: false
          TargetOriginId: !Ref BucketName
          ViewerProtocolPolicy: allow-all
        DefaultRootObject: index.html
        Enabled: true
        Origins:
          - DomainName: !Ref BucketRegionalDomainName
            Id: !Ref BucketName
            #CustomOriginConfig:
            OriginAccessControlId: !GetAtt OAC.Id
            S3OriginConfig:
              OriginAccessIdentity: ""
        PriceClass: PriceClass_All
Code language: YAML (yaml)

The key item in the CloudFront configuration for using OAC is the Origins property.
The ID of the OAC defined earlier is set to the OriginAccessControlId property.

The S3OriginConfig property is also important.
Specify an empty string for the internal OriginAccessIdentity property.
If you do not set the above, the following error will occur when creating the CloudFormation stack.

Invalid request provided: Exactly one of CustomOriginConfig and S3OriginConfig must be specified

Note that the CustomOriginConfig property is a parameter for S3 buckets with static website hosting enabled, so do not set it.

Bucket Policy

Resources:
  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref BucketName
      PolicyDocument:
        Statement:
          Action:
            - s3:GetObject
          Effect: Allow
          Principal:
            Service:
              - cloudfront.amazonaws.com
          Resource: !Sub "arn:aws:s3:::${BucketName}/*"
          Condition:
            StringEquals:
              AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${Distribution}"
Code language: YAML (yaml)

Define the bucket policy with reference to the following official AWS page.

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html

If the aforementioned CloudFront distribution is the principal, the content allows the action to retrieve the object.

(Reference) CloudFormation Custom Resources

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt Function.Arn

  Function:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import os

          bucket_name = os.environ['BUCKET_NAME']

          object_name = 'index.html'
          object_body = """<html>
            <head></head>
            <body>
              <h1>index.html</h1>
              <p>{bucket_name}</p>
            </body>
          </html>""".format(bucket_name=bucket_name)
          content_type = 'text/html'
          char_code= 'utf-8'

          s3_client = boto3.client('s3')

          CREATE = 'Create'
          DELETE = 'Delete'
          response_data = {}

          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                put_response = s3_client.put_object(
                  Bucket=bucket_name,
                  Key=object_name,
                  Body=object_body.encode(char_code),
                  ContentEncoding=char_code,
                  ContentType=content_type)
                print(put_response)

              elif event['RequestType'] == DELETE:
                list_response = s3_client.list_objects_v2(
                  Bucket=bucket_name)
                for obj in list_response['Contents']:
                  delete_response = s3_client.delete_object(
                    Bucket=bucket_name,
                    Key=obj['Key'])
                  print(delete_response)

              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)

            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      Environment:
        Variables:
          BUCKET_NAME: !Ref BucketName
      FunctionName: !Sub "${Prefix}-function"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
Code language: YAML (yaml)

Use a CloudFormation custom resource to automatically create an object in an S3 bucket.
In this case, we will create index.html in the aforementioned S3 bucket.

For more information, please refer to the following page

https://awstut.com/en/2022/05/08/create-and-delete-s3-object-by-cfn-custom-resource-en

Architecting

Using CloudFormation, build this environment and check the actual behavior.

Create CloudFormation stacks and check resources in stacks

Create a CloudFormation stack.
For information on how to create stacks and check each stack, please refer to the following page

https://awstut.com/en/2021/12/11/cloudformations-nested-stack

After checking the resources in each stack, information on the main resources created this time is as follows

  • CloudFront distribution: E3YPSL0CON945
  • CloudFront domain name: d1ujnv40lgk4sa.cloudfront.net
  • OAC: fa-096
  • S3 bucket: fa-096

Confirm the created resource from AWS Management Console.
Check the OAC.

Detail of CloudFront 1.

OAC is created.
You can see that it is configured to always enforce signing with SigV4.

You can also see that it is associated with the CloudFront distribution.

Detail of CloudFront 2.

If you check the configuration page on the CloudFront distribution side, you will see that the OAC setting is enabled.

We can also see that an S3 bucket is specified as the origin.

Detail of S3 1 .

The index.html file is placed in the bucket.
The HTML file was automatically generated and placed by the Lambda function associated with the CloudFormation custom resource.

Detail of S3 2.

Checking the bucket policy, we can see that it allows access to the S3 bucket with CloudFront as the principal.

Check Operation

Now that everything is ready, access the CloudFront distribution.

Detail of CloudFront 3.

We were able to access the HTML file (index.html) in the S3 bucket.
This means that access to objects in the bucket, originally restricted by the bucket policy, is now allowed via CloudFront by using OAC.

Next, access the S3 bucket directly.

Detail of CloudFront 4.

Denied.
This means that the bucket policy is in action and direct access is restricted.

SUMMARY

We have shown how to access S3 buckets via CloudFront using OAC.