The other day I noticed a very irritating behavior on my personal website that I built using Angular. When I type in my domain name in the browser URL bar
I get redirected to elbanhawy.com/home
and can see my website homepage load. That's not the annoying behavior, that the expected part. What I found out by accident was that if I tried to reload that page or enter a specific path on my website like elbanhawy.com/blog
I get an Access Denied response from AWS!
What's happening here??
This might be a common shocker to anyone who's never deployed an SPA like React, Vue, and Angular on AWS S3 and CloudFront before so let's see how I tackled this issue.
Step 1: Debugging Checklist
Checking S3 bucket policies and the CF Origins and OAI configurations
My angular site is hosted on an S3 bucket which is not public. My CloudFront distribution serves the S3 bucket content, so my S3 bucket policies should explicitly allow my CloudFront distribution to access all content inside the bucket. Let's take a look:
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E3LLNG3QXXXXX"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-angular-website/*"
}
]
}
The bucket policy above explicitly allows my CloudFront OAI that I have associated with my CloudFront distribution to access/get all the objects inside the S3 bucket. I take note of that OAI ID E3LLNG3QXXXXX which I will need later.
I double check in my CloudFront distribution Origins tab to see if my S3 bucket is listed as an origin.
I see that it is so I choose it and click "Edit" to see its configuration details:
I make sure that under S3 bucket access section that the "Yes use OAI" option is selected.
Now I take note of the OAI name selected above. I go to CloudFront's console (CF/Security/Origin access identities) to see my listed OAIs. Remember that OAI ID I took note of from the S3 bucket policy? Now I need to make sure that it is present in the list.
I see that it is so I can safely assume that the CloudFront distribution that serves my website has correct access to my S3 bucket.
Okay…So what else can be wrong??
Step 2: Solution
At this point I'm scratching my head and trying to read countless AWS docs for hints without success. But then I spot an interesting tab in my CloudFront's distribution page.
Come to think of it, when I enter my root domain I didn't get any errors initially. It is only when I reload or include a specific route in my URL that I get an Access Denied error, which translates to a 403 error code.
So I decide to click "Create custom error response" and add an entry:
I choose 403:Forbidden from the list of error codes, then select "Yes" to Customize error response and then, and this is the critical part, I enter "/index.html" as the Response page path and 200: OK as the HTTP Response code.
Now if I enter my blog path URL in the browser and hit enter I get served the correct page without any Access Denied error even when I reload the page!
Note: if you have more than one CloudFront distribution for your S3 website, you need to these steps for each one. For example, I had a separate distribution for my www
subdomain and had to apply the solution there as well as the distribution for the root domain (elbanhawy.com
).
Step 3: Reflection
I've solved the problem but what's still missing is a clear explanation of why this Access Denied error showed up in the first place.
Well this can be attributed to the way S3 buckets behave in response to requests. S3 does not understand routes. An S3 bucket is like a server that is expecting a path to a specific file stored in it. For example, I can be more explicit and add /index.html
at the end of my domain url and I would still get the homepage. S3 understands this because it looks for a file called index.html
in the bucket and returns it.
Originally, when I typed in my domain url elbanhawy.com
, because of my configurations, CloudFront automatically appended /index.html
to the request it forwarded to the S3 bucket which S3 understood and return my index.html file which had links to my compiled angular code. When loaded in the browser, the angular application takes over and the angular router takes effect.
However, when I reloaded the page or type the route in my url, CloudFront forwarded the request to S3 as-is and S3 tries to find a literal match for a file with the same route name. For example, when I tried elbanhawy.com/blog
before the fix, S3 tried to look for a file called blog
which does not exist so it returns an error. Since I have not originally specified how errors should be handled by CloudFront, an Access Denied error was returned to the browser.
The solution above, changed that default error behavior to always return the index.html
file whenever that error occurs and therefor the angular site is always returned and the angular router still gets access to the route in the URL and is able to show me the correct page.
For more tips and insights on cloud and web development follow me on Twitter @adham_benhawy.
Top comments (3)
Hey, i recently was facing the same issue of access denied.
You need to give S3:ListBucket access to CloudFront to avoid the 403 error.
Hi Roshan. That's interesting, I tried to change my S3 bucket policy to give CloudFront S3:ListBucket access as well as S3:GetObject (also tried just S3:ListBucket on its own). I was still greeted with 403 error afterwards. Maybe you had a different setup or S3 access pattern.
Thanks Adham El Banhawy , You really saved my day.