Securely Serving a Static Site on AWS S3 with CloudFront
Tags: aws, cloud, s3
Published: 2021-01-13 19:06:28

For any sadists out there who enjoy the quirkiness of the the AWS permissions ecosystem, you'll love this one.

I've been working on a project recently that involves hosting a static site on S3 and serving it via CloudFront CDN. AWS has some [good documentation](https://aws.amazon.com/premiumsupport/knowledge-center/cloudfront-serve-static-website/) on the process but I ended up running into a problem that I found to be incredibly frustrating. Since I spent about a full day getting way too deep into the docs on this issue, I thought I'd spell it out for any other poor souls that run into the same trouble:
<br/>
<br/>

## Public Read Access to a Bucket Makes me Nervous
When you setup your bucket with a web-hosting configuration the [docs](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/static-website-hosting.html) instruct to use a bucket policy that allows public read access such as:
``` json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::example.com/*"
            ]
        }
    ]
}
```
<br/>

Then your content gets served via a website similar to <code>mybucket.com.s3-website-<region>.amazonaws.com</code>. 

Now, we've just given public read access to our bucket, but we want to serve the site through CloudFront only. We don't want people using the direct S3 website endpoints, we want to restrict it to our CloudFront configuration. Usually what AWS suggests is to use an Origin Access Identity and update the bucket policy to restrict to that OAI. 

But if there's one thing I've learned while using AWS, it's that there's always one little info box that can suddenly end days of failed deployments and poring through docs. In this case, it's this one in these [docs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html):
<br/>
![Screen Shot 2021-01-13 at 2.43.54 PM](https://dev-to-uploads.s3.amazonaws.com/i/jm2nd48evbllpkpsl2q4.png)
<br/>

These are the docs on configuring CloudFront for S3 content, and they explicitly state that you can't restrict bucket access when using a website configuration. What they suggest is using a custom header in your CloudFront configuration instead and conditionally requiring that header in the bucket policy. 
<br/> 
<br/>

## Using a Custom Header to Secure Bucket Access
To implement this, you can go into your distribution's settings, and add a custom header:
<br/>
![Screen Shot 2021-01-13 at 2.55.50 PM](https://dev-to-uploads.s3.amazonaws.com/i/aacaffemby3wqn4hhb4m.png)
<br/>

For the value field, use a value that's unique, or something that only you know. I just used the Python <code>secrets.token_urlsafe()</code> [function](https://docs.python.org/3/library/secrets.html#secrets.token_urlsafe)
<br/>
 
Then we can update the bucket policy to read this:
``` json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudFrontGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::example.com/*"
            ],
            "Condition": {
             "StringLike": {
                "aws:Referer": "gQye0hsIAIsnEnX1EIwUNVLa5Tnly5UFthZrx6n7-gI"
              }
            }
        }
    ]
}
```
Notice the last key "Condition". We use a "StringLike" match on the Referer header on the request to ensure that the we only allow access to requests that match our unique key. Now, when we try to access our S3 website via the object URLs instead of through CloudFront we'll get an Access Denied error and we've successfully secured our bucket content.

In the event that this post didn't help at all, here's a couple of articles that should point you in the right direction:
- [AWS: Serve a static site](https://aws.amazon.com/premiumsupport/knowledge-center/cloudfront-serve-static-website/)
- [Reddit: 6 upvotes](https://www.reddit.com/r/aws/comments/b6r9xe/is_there_a_way_to_restrict_access_to_a_s3_static/ejmtsdw/?context=8&depth=9)