Hosting S3 Static Website using CloudFront with OAI
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
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 aliasd123456.cloudfront.com
which is the CloudFront distribution.- The user’s browser downloads the website’s CloudFront distribution. If there’s a cache hit, the distribution returns the object immediately.
- The SSL certificate is managed in
Amazon ACM
and configured in CloudFront during the creation of the distribution.- The CloudFront distribution is replicated across all edge locations of AWS.
- If extra logic handling is needed, a
Lambda@Edge
can be deployed on the edge locations to do additional processing.- 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.
- The requested object is returned to the distribution.
Pre-requisites
Before creating the CloudFront distribution, ensure that you have the following items ready.
- An S3 bucket with the static content of the website.
- A registered domain name.
- An SSL certificate in
AWS Certificate Manager
orACM
.
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.
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.
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.
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 |
- 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. - 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. - 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.
- 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 iscode-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.
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.
Origins tab
In the origins tab is where you will find the OAI, the Origin ID we gave and the S3 origin domain name.
Behaviors tab
The DefaultCacheBehavior property values can be seen in the behaviors tab.
If you edit the behavior item, you will find more settings including the GET and HEAD methods we set in the template.
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.
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
and blocking 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.