51

For a while, I was simply storing the contents of my website in a s3 bucket and could access all pages via the full url just fine. I wanted to make my website more secure by adding an SSL so I created a CloudFront Distribution to point to my s3 bucket.

The site will load just fine, but if the user tries to refresh the page or if they try to access a page using the full url (i.e., www.example.com/home), they will receive an AccessDenied page.

enter image description here

I have a policy on my s3 bucket that restricts access to only the Origin Access Identity and index.html is set as my domain root object.

I am not understanding what I am missing.

To demo, feel free to visit my site.

You will notice how it redirects you to kurtking.me/home. To reproduce the error, try refreshing the page or access a page via full URL (i.e., kurtking.me/life)

Any help is much appreciated as I have been trying to wrap my head around this and search for answers for a few days now.

8.27.2023 Disclaimer

The solution I marked as the answer worked for me at the time, and held me over as I was developing locally and learning. It has come to my attention that this may not be the best practice.

I have not spent enough time in this area of AWS to have a recommended alternative, but some ideas are floating throughout the Answer section.

Kurt King
  • 1,952
  • 2
  • 11
  • 12
  • Have you raised a support case with AWS? Assuming you have, or are willing to sign up for, at least developer support (https://aws.amazon.com/premiumsupport/developer-support/). You can always cancel the support option at the end of the first month, if no longer needed. – jarmod Jun 02 '17 at 13:27
  • I have explored that option, but wanted to see if anyone else in the community has had any similar issues before doing so. – Kurt King Jun 02 '17 at 13:33
  • 1
    Your page is engaging in some kind of SPA-style monkey business. When you click on /life or the other pages, the address bar changes, but there's no HTTP request to `GET /life` -- the content is obviously not a conventional web page load, so that explains why it works when clicking the links but not when trying to load the other "pages" directly... there do not appear to be any other real pages, because all the content is in the JavaScript. This is fundamentally an Angular issue, and how to configure server-side redirects to do something that makes Angular work. – Michael - sqlbot Jun 03 '17 at 01:23
  • I only have a passing familiarity with Angular, but I did write [this](https://serverfault.com/a/633571/153161), which might be related to your ultimate solution. – Michael - sqlbot Jun 03 '17 at 01:26
  • 1
    +1 - It did not answer my question exactly, but it started leading me in the right path to search more specifically using keywords like 'SPA, s3/cloudfront issues'. This ultimately lead me an article I posted at the bottom of my solution. – Kurt King Jun 03 '17 at 03:34

7 Answers7

131

I have figured it out and wanted to post my solution in case anyone else runs into this issue.

The issue was due to Angular being a SPA (Single Page App) and me using an S3 bucket to store it. When you try to go to a specific page via url access, CloudFront will take (for example, /about) and go to your S3 bucket looking for that file. Because Angular is a SPA, that file doesn't technically exist in your S3 bucket. This is why I was getting the error.

What I needed to do to fix it

If you open your distribution in Cloudfront, you will see an 'Error Pages' tab. I had to add two 'Custom Error Responses' that handled 400 and 403. The details are the same for 400 and 403, so I only include a photo for 400. See below: enter image description here

enter image description here

Basically, what is happening is you are telling Cloudfront that regardless of a 400 or 403 error, to redirect back to index.html, thus giving control to Angular to decide if it can go to the path or not. If you would like to serve the client a 400 or 403 error, you need to define these routes in Angular.

After setting up the two custom error responses, I deployed my cloudfront solutions and wallah, it worked!

I used the following article to help guide me to this solution.

Kurt King
  • 1,952
  • 2
  • 11
  • 12
  • I dont see such UI in my aws cloud front panel. – Swapnil Mhaske Mar 06 '18 at 10:00
  • 2
    I didn't see the subtle error ... S3 was issuing a 400 instead of 404. To make it issue a 404 I allowed the `s3:ListBucket` permission... A quicker fix given it would take 20+ mins to reconfigure the CF distribution :) – Sunil D. May 09 '18 at 23:00
  • Nice find, I'll take it :) – DraganescuValentin Sep 27 '18 at 13:16
  • Exactly my issue, my use case (but with VueJS). THANK YOU! – babis21 Apr 17 '20 at 09:24
  • I actually want in the reverse. There is a cloudways url, domain url and cdn url. only domain url should be valid and both of others should say access denied when on the direct hit.? can we do so? – sanjeev shetty Oct 23 '20 at 08:29
  • This solved the issue, but now I see this in the response headers: "x-cache: Error from cloudfront". I'm worried this may cause the request to take more time. Is it possible to get a "Hit from cloudfront" response as in the other requests? – LachoTomov Jan 20 '21 at 11:54
3

The better way to solve this is to allow list bucket permission and add a 404 error custom rule for cloudfront to point to index.html. 403 errors are also returned by WAF, and will cause additional security headaches, if they are added for custom error handling to index.html. Hence, better to avoid getting 403 errors from S3 in the first place for angular apps.

Varun
  • 74
  • 1
2

If you have this problem using CDK you need to add this :

MyAmplifyApp.addCustomRule({
  source: '</^[^.]+$|\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|ttf|map|json)$)([^.]+$)/>',
  target: '/index.html',
  status: amplify.RedirectStatus.REWRITE
});
tuardoui
  • 69
  • 7
1

For those who are trying to achieve this using terraform, you only need to add this to your CloudFront Configuration:

resource "aws_cloudfront_distribution" "cf" {
   ...

   custom_error_response {
   error_code    = 403
   response_code = 200
   response_page_path = "/index.html"
   }
   
   ...
   
}
0

The accepted answer seems to work but it does not seem like a good practice. Instead check if the cloudfront origin is set to S3 Bucket(in which the static files are) or the actual s3 url endpoint. It should be the s3 url endpoint and not the s3 bucket.

The url after endpoint should be the origin

enter image description here

Deep Patel
  • 619
  • 7
  • 8
  • 1
    The 2 endpoints you've mentioned here are the S3 website endpoint and the S3 REST API endpoint. The website endpoint approach with CloudFront that you are referring to here still requires you to specify the error page, which in the case of an SPA is `index.html`, else full URLs generate a 404. This is pretty much the same as going with the REST API endpoint and having custom Error Pages, as described in the accepted answer. Reference: https://aws.amazon.com/premiumsupport/knowledge-center/s3-rest-api-cloudfront-error-403/ – Ash Aug 26 '20 at 00:34
  • if you have SPA(Angular, react etc) app on s3 and you serve with CloudFront, you don't have to enable s3 static website hosting. Every body is getting confused about this. You gave access to CF for files with Object Access Identity. CF and OAI reaches the files on behalf of the users. – smile Sep 03 '21 at 07:12
0

Add a Cloudfront function to rewrite the requested uri to /index.html if it doesn't match a regex.

For example, if none of your SPA routes contain a "." (dot), you could do something like this:

function handler(event) {
    var request = event.request
    if (/^(?!.*\..*).*$/.test(request.uri)) {
        request.uri = '/index.html'
    }
    return request
}

This gets around any kind of side effects you would get by redirecting 403 -> index.html. For example, if you use a WAF to restrict access by IP address, if you try to navigate to the website from a "bad IP", a 403 will be thrown, but with the previously suggested 403 -> index.html redirect, you'd still see index.html. With a cloudfront function, you wont.

  • Is this something you've tested? I would expect that any traffic that is blocked by the WAF will never reach CloudFront and only 403s produced by the CloudFront's origins will follow these rules. – Chisholm May 02 '22 at 16:35
  • Yes, the WAF passes the 403 to Cloudfront as you may have a custom response configured for the 403. – Alex Mills May 03 '22 at 17:47
  • This works for me, many thanks. Though, I have a second SPA deployed under "/admin/". How would you adapt the function to also cover that? – fred_online May 31 '22 at 12:33
-1

Please check from the console which error are you getting. In my case I was getting a 403 forbidden error, and using the settings which are shown in the screenshot worked for me

screenshot

karel
  • 5,489
  • 46
  • 45
  • 50