Ghost's forceAdminSSL redirect loop

Ghost has brief documentation of configuration of HTTPS support.

If you want both HTTP and HTTPS accessible, but want to force HTTPS for the administration back-end, it suggests config.js resemble:

  1. url: 'http://www.foreverlarz.com'
  2. forceAdminSSL: true

But if you only follow these two configuration options, you can get stuck in a redirect loop if you visit the administration back-end.

Why? Because Ghost is served by proxy, and Ghost doesn't know if the proxy is serving over HTTP or HTTPS to the client, unless you tell it so. Instead of handling this lack of knowledge gracefully, Ghost redirects you around in a loop.

So how do you tell Ghost whether it is being served by HTTPS or HTTP?
Send Ghost a header of X-Forwarded-Proto "https" if it's being served securely, and X-Forwarded-Proto "http" if insecurely.

I think that the configuration documentation should say that forceAdminSSL depends on the X-Forwarded-Proto header. Why mention the configuration option, if the necessary dependency isn't described? And, yes, the doc links to an example nginx configuration, but I doubt many Apache users would inspect this and carefully port the configuration to Apache. (I do have the proper configuration for Apache below; read on!)

Apache Configuration

For Apache, you might have two virtual hosts as follow.

HTTP Virtual Host

<VirtualHost 10.0.0.4:80>  
    ServerName http://www.foreverlarz.com:80
    ServerAlias foreverlarz.com

    RewriteEngine On
    RewriteCond %{HTTP_HOST} !^www\.foreverlarz\.com$ [NC]
    RewriteRule ^/?(.*)$ http://www.foreverlarz.com/$1 [L,R=301]

    RequestHeader set X-Forwarded-Proto "http" ##### you need this!

    ProxyRequests off
    ProxyPass / http://127.0.0.1:29900/
    ProxyPassReverse / http://127.0.0.1:29900/
</VirtualHost>  

HTTPS Virtual Host

<VirtualHost 10.0.0.4:443>  
    ServerName https://www.foreverlarz.com:443
    ServerAlias foreverlarz.com

    RewriteEngine On
    RewriteCond %{HTTP_HOST} !^www\.foreverlarz\.com$ [NC]
    RewriteRule ^/?(.*)$ https://www.foreverlarz.com/$1 [L,R=301]

    SSLEngine on
    SSLOptions +StrictRequire
    SSLCertificateKeyFile /etc/letsencrypt/live/www.foreverlarz.com/privkey.pem
    SSLCertificateFile /etc/letsencrypt/live/www.foreverlarz.com/fullchain.pem
    # HSTS (mod_headers is required) (15768000 seconds = 6 months)
    Header always set Strict-Transport-Security "max-age=15768000"

    RequestHeader set X-Forwarded-Proto "https" ##### you need this!

    ProxyRequests off
    ProxyPass / http://127.0.0.1:29900/
    ProxyPassReverse / http://127.0.0.1:29900/
</VirtualHost>  

Best-Practice HTTPS Configuration

While we're at it, let me mention that this assumes you have the rest of the HTTPS configuration elsewhere. I like to use /etc/apache2/conf-enabled/local.conf for global configuration. Use Mozilla's handy configuiration generator to see what you should put here. I use the "old" version to maximize compatibility with older clients and fly in the face of NIST and HIPAA compliance. Here's my local.conf, for fun:

ServerName www.mothership.com  
ServerTokens Prod  
ServerSignature Off  
TraceEnable Off  
Header set X-Content-Type-Options: "nosniff"  
Header set X-Frame-Options: "sameorigin"

# old configuration, tweak to your needs
SSLProtocol             all  
SSLCipherSuite          ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:SEED:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:!SRP  
SSLHonorCipherOrder     on  
SSLCompression          off

# OCSP Stapling, only in httpd 2.3.3 and later
SSLUseStapling          on  
SSLStaplingResponderTimeout 5  
SSLStaplingReturnResponderErrors off  
SSLStaplingCache        shmcb:/var/run/ocsp(128000)  

Now you should have Apache configured to serve Ghost over HTTP and HTTPS, forcing HTTPS for the back-end, and getting an A+ from Qualys's SSL Test and High-Tech Bridge's SSL Test!

Good luck!

P.S. 'Cause I like to show off, here are my SSL test results for Qualys and Bridge-Tech—pretty good, huh? My Mozilla Observatory score sucks, though. One rainy day, I'll work on that...