← michaelfulton.co

Ghost on EC2 with CloudFront http/https Redirect Loop

This issue can also occur with WordPress on DigitalOcean with Cloudflare, as well as with [an application] on [a non-https server] with [an https CDN].

Most of AWS resources can utilize the free SSL/TLS certificates provided for your domain name through ACM. CloudFlare turns on secure certificates by default. But if your application is running on an EC2 instance, all bets are off because you don't have access to the certificate private keys stored in ACM.

However, you can still serve your EC2 content securely by layering a CloudFront distribution in front of the EC2. This way, the viewer connects to CloudFront using https and the response from your server will be reverse-proxied back to the viewer over https. There is an unsecure connection between CloudFront and EC2, but chances are these products are running in the same data center, and any inter-data center communication at AWS is probably wrapped securely. So keep that in mind if using a combination of hosting provider and CDN that aren't managed by the same company.

The infinite loop occurs for the reason described on https://ghost.org/docs/faq/proxying-https-infinite-loops/:

Your proxy will handle SSL termination and proxy to Ghost using http. To tell Ghost that the requests coming into the proxy are secure, set the x-forwarded-proto header to https. Without this, Ghost will think the requests are insecure, attempt to redirect to the https version of a URL and cause an infinite redirect loop.

As it mentions, we can't leave proxy_set_header X-Forwarded-Proto on $scheme. Since this will always be http from CloudFront, but we know the viewer has a secure connection into AWS, we can hard code it to https. Your config could look something like this:

server {
    listen 80;
    listen [::]:80;

    server_name my-ghost-site.com;
    root /var/www/my-ghost-site/system/nginx-root; # Used for acme.sh SSL verification (https://ac>
    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto <span style="font-weight:bold;">https</span>;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;

    location ~ /.well-known {
        allow all;

    client_max_body_size 50m;
A sample Nginx configuration file for Ghost.

Also take a look at the Behaviors tab on your CloudFront distribution. For Ghost, enable all the HTTP methods such as PUT, POST, and DELETE, as the Ghost admin panel will need these methods to operate.

You can customize the caching policy. I turned off caching for my admin panel, but added another Behavior with caching on for the /content/images folder. You can also customize the origin request policy. The default AllViewer forwards all cookies, headers, and query strings from the Viewer to the Origin. That seems right to me. Your CloudFront config could look something like this:

I hope this helps! I'll try to answer any questions in the comments section.