An unsecure website is not acceptable these days. If you’re hosting your website using AWS S3 bucket’s static website hosting attribute, its one limitation is that your pages are hosted using http only and browsers will report this as Not Secure. This does not give a good impression to your visitors.

Another security compromise that you have to make, and the more critical one, is that you need set your S3 bucket publicly readable. By default, this is not recommended by AWS. More and more security breaches are happening due to wrongly configured permissions of S3 buckets.

So how do we solve this? Use CloudFront with Object Access Identity or (OAI).

CloudFront is the AWS CDN solution where you can target your private S3 bucket as the origin using OAI. This will be the identity defined in your S3’s bucket policy to grant permission only to the CloudFront distribution and nobody else.

CloudFront also ensures the data in transit are in https and secure.

Here’s the overview of the set-up using CloudFront.

Architecture Overview

architecture-cloudfront-website

  1. Route 53 resolves the domain name to the target CloudFront distribution. For example, in this website, code.eidorian.com is registered in Route 53 and it resolves it to the target alias d123456.cloudfront.com which is the CloudFront distribution.
  2. The user’s browser downloads the website’s CloudFront distribution. If there’s a cache hit, the distribution returns the object immediately.
  3. The SSL certificate is managed in Amazon ACM and configured in CloudFront during the creation of the distribution.
  4. The CloudFront distribution is replicated across all edge locations of AWS.
  5. If extra logic handling is needed, a Lambda@Edge can be deployed on the edge locations to do additional processing.
  6. The private S3 bucket containing the static website is accessed by the CloudFront distribution via the granted permission given to its OAI in the S3’s bucket policy.
  7. The requested object is returned to the distribution.

Pre-requisites

Before creating the CloudFront distribution, ensure that you have the following items ready.

  1. An S3 bucket with the static content of the website.
  2. A registered domain name.
  3. An SSL certificate in AWS Certificate Manager or ACM.

Set-up the S3 bucket static content

The S3 bucket contains the website. Have something like index.html at least for testing and an error page like error.html

In my case, I am using Jekyll which is a static website generator. It has an index.html and a 404.html error page. We will be using that in this example.

Register a domain name

You can use Route 53 or some other domain name registrar to register your domain.

route53-register-domain-name

Create an SSL certificate in AWS Certificate Manager

If you don’t have a certificate yet, create one for your registered domain name in ACM.

Important: Create the certificate in the us-east-1 N. Virginia region. CloudFront will only see the certificates in this region.

Make sure that all the CNAMEs that you will use in CloudFront are also included in the certificate. Here I’m adding both eidorian.com and code.eidorian.com.

acm-add-domain-names

Then wait for the validation status to be Success.

If it’s Pending for quite a while, check the details. It may be waiting for an action from you like adding a record to Route 53.

acm-domain-names-status

Take note of the certificate’s ARN. You will need it later in the parameters section.

Create the CloudFront distribution using CloudFormation

Okay, so now that we have all the pre-reqs ready, let’s create the CloudFront distribution. It’s not very exciting to use the AWS console, let’s do it the CloudFormation way!

I have prepared a re-usable template below with three input Parameters. These are the three pre-requisites mentioned above - bucket name, SSL cert, and the CNAMEs.

CloudFormation Template

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  BucketName:
    Description: S3 Bucket name
    Type: String
  SSLCert:
    Description: ACM certificate arn
    Type: String
  DomainNames:
    Description: Domain names or CNAMEs
    Type: CommaDelimitedList
Resources:
  WebsiteDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Aliases: !Ref DomainNames
        Origins:
        - DomainName: !Join ['', [!Ref BucketName, '.s3.amazonaws.com']]
          Id: !Join ['', [!Ref BucketName, 'S3OriginId']]
          S3OriginConfig:
            OriginAccessIdentity: !Join ['', ['origin-access-identity/cloudfront/', !Ref CloudFrontOAI]]
        Enabled: 'true'
        Comment: !Join ['', ['CloudFront for S3 bucket ', !Ref BucketName]]
        DefaultRootObject: index.html
        CustomErrorResponses:
          - ErrorCode: 404
            ResponseCode: 200
            ResponsePagePath: /404.html
          - ErrorCode: 403
            ResponseCode: 200
            ResponsePagePath: /404.html
        DefaultCacheBehavior:
          AllowedMethods:
          - GET
          - HEAD
          TargetOriginId: !Join ['', [!Ref BucketName, 'S3OriginId']]
          ForwardedValues:
            QueryString: 'false'
            Cookies:
              Forward: none
          ViewerProtocolPolicy: redirect-to-https
        ViewerCertificate:
          AcmCertificateArn: !Ref SSLCert
          MinimumProtocolVersion: TLSv1
          SslSupportMethod: sni-only
  CloudFrontOAI:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Join ['', [!Ref BucketName, '-origin-access-identity']]

In the Resources section, we have two types. One is the CloudFront distribution AWS::CloudFront::Distribution and the other one is the OAI AWS::CloudFront::CloudFrontOriginAccessIdentity.

AWS::CloudFront::Distribution

In the CloudFront distribution resource, we map the parameter values to the distribution properties.

Property Parameter Example
Aliases DomainNames eidorian.com,code.eidorian.com
DomainName BucketName mybucket.s3.amazonaws.com
AcmCertificateArn SSLCert arn:aws:acm:us-east-1:youraccount:certificate/1234
OriginAccessIdentity via Reference CloudFrontOAI
  1. The Aliases property sets the CNAMEs of the distribution. This is required later when setting the Route53 record to target the distribution alias. In the DomainNames parameter, put your registered domain name including the alternate names.
  2. The DomainName property is the target origin domain name of the distribution where the CloudFront will get its content. In this case, it is the S3 bucket containing the website. The CloudFormation template uses the BucketName parameter to set this property by concatenating the bucket name with the .s3.amazonaws.com suffix. This suffix is the AWS domain name for S3 buckets.
  3. The AcmCertificateArn property tells CloudFront which SSL certificate to use. Here the parameter SSLCert defines this with the ARN string of the certificate in ACM.

    Double-check your cert ARN, it should be in the us-east-1 region.

  4. The OriginAccessIdentity property is the key property here that tells CloudFront which ID to use when accessing the origin (the S3 bucket). There is no parameter passed to this since we do not know yet the OAI prior to the CloudFormation stack creation. To get a hold of the reference of the OAI, use the OAI Resource’s name as reference which is CloudFrontOAI and it requires a prefix of origin-access-identity/cloudfront/.

For the other properties of the distribution, you can look them up here for details. But briefly, what we configured here is that CloudFront will default to index.html in the root folder. If the S3 origin returns 404 Not Found or 403 Forbidden, CloudFront will display the error page 404.html and remap the response to HTTP 200.

For convenience, some of the properties like IDs and comments are set by the template automatically using the bucket name. For example, the Origin ID is set to {BucketName}S3OriginId. You can change this string value if you want.

AWS::CloudFront::CloudFrontOriginAccessIdentity

This is the OAI resource that creates the Origin Access Identity with the name CloudFrontOAI. It simply creates the OAI and assigns a comment for description purpose.

If you already have an existing OAI and want to re-use it, you can just pass it’s ID as a parameter to set the OriginAccessIdentity. You won’t need the OAI resource in the template.

CloudFormation JSON property file

We can pass the parameter values to the template via command line option, AWS console, or using a property file. We will use the last one to create the CloudFormation stack.

Here’s a sample property file of the parameters and their values.

[
    {
        "ParameterKey": "BucketName",
        "ParameterValue": "code.eidorian.com"
    },
    {
        "ParameterKey": "SSLCert",
        "ParameterValue": "arn:aws:acm:us-east-1:youraccount:certificate/11111111-1111-1111-1111-111111111111"
    },
    {
        "ParameterKey": "DomainNames",
        "ParameterValue": "eidorian.com,code.eidorian.com"
    }
]

Executing the CloudFormation template using AWS CLI

Alright. We are all set.

Open a terminal and run the AWS CLI to create the stack.

In the sample commands, the template file name is cloudfront-s3-origin.yaml and the property file name is code-eidorian-com-properties.json

Create stack

aws cloudformation create-stack --stack-name cloudfront-s3-code-eidorian-com \
--template-body file://./cloudfront-s3-origin.yaml \
--parameters file://./code-eidorian-com-properties.json

Delete stack

If something goes wrong and your stack rolls back, delete the stack and re-create again.

aws cloudformation delete-stack --stack-name cloudfront-s3-code-eidorian-com

Update stack

If you update some of the properties in the template, simply update the stack.

aws cloudformation update-stack --stack-name cloudfront-s3-code-eidorian-com \
--template-body file://./cloudfront-s3-origin.yaml \
--parameters file://./code-eidorian-com-properties.json

The CloudFront distribution creation could take several minutes (~30 mins) to complete. The reason for this is it that it updates all the edge locations and distributes your website content. Even the delete and update stack could take the same amount of time.

cloudfront-deployed-status

Wait for your distribution status until it says Deployed. Then go to the distribution and verify the settings. Hopefully everything went well and your CloudFront distribution was created successfully with all the correct properties in place.

Verify the CloudFront distribution

Open your distribution and look at the tabs.

General tab

In the general tab you will see the CNAMEs you put in the Alias property, the SSL certificate ARN and a link to it, the index.html as the default root object, the sni-only in the SSL supported method, the minimum protocol TLSv1 and the comment set by the template.

cloudfront-general-tab

Origins tab

In the origins tab is where you will find the OAI, the Origin ID we gave and the S3 origin domain name.

cloudfront-origins-tab

Behaviors tab

The DefaultCacheBehavior property values can be seen in the behaviors tab.

cloudfront-behaviors-tab

If you edit the behavior item, you will find more settings including the GET and HEAD methods we set in the template.

cloudfront-behavior-edit

Error pages tab

Lastly, in the error pages tab, where we set the 401.html page as the default error page for errors 404 and 403 can be verified here.

cloudfront-error-pages-tab

Update the S3’s bucket policy

Now that the CloudFront distribution has been created and verified, there’s just one last thing you need to do before testing it out. Tell the S3 bucket to allow the OAI to access its content. Here’s the part where you update the S3 bucket’s policy and make it private allowing only the OAI arn as the Principal to access the bucket and no one else.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Allow-OAI-Access-To-Bucket",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E1111111111111"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::s3bucket/*"
        }
    ]
}

Then, you can now safely make your S3 private by setting the S3 static website to disabled

s3-disable-static-website

and blocking all public access.

s3-block-all-public-access

Test your new secure website

That’s all folks. Now try and hit your website using https. The browser should now say it is secure. If you try an invalid path or page, you should see the default error page. If you try going to your S3 bucket’s direct url like the index.html S3 url, the access will be denied.

Final thoughts

A lot steps here and I tried to explain as much detail as I can but hopefully this is helpful especially the re-usable CloudFormation template. I removed the Lambda@Edge part since it is optional and this is getting long. I will talk about it more on my next post. Let me know your comments below.