Ghost on Azure - (310) too many redirects with https problem solved

This blog is published with Ghost CMS on Microsoft Azure Web Apps platform. Ghost is available in Azure Marketplace for some time and You can run it with few clicks - if You are OK with free Web Apps plan's limitations it will be also free of charge. Sounds OK...
But there is a small (but very important) issue with Ghost's default configuration on Azure (except that there is an out of date version of Ghost available in Marketplace).

Too many redirects with HTTPS enabled

If You enable HTTPS for whole site natively (changing http to HTTPS in Ghost's configuration file: config.js) or force HTTPS for admin panel only with forceAdminSSL: true directive in config.js, it will not work at all and browser will return the HTTP 310 error: ERRTOOMANY_REDIRECTS.

Workaround

There is a workaround for this issue and it's implemented in default Ghost configuration on Azure. The default configuration is set to HTTP for whole site and the HTTP to HTTPS redirection (via rewrite) is made in web.config file for /ghost/ subdirectory (the admin panel). So every request with /ghost/ subdirectory is redirected to HTTPS. And it works but it's highly unsafe.

Why we should not use workaround?

In typical blogging it will work and there will be no problems. But there is (at least) one situation I found, where this workaround will fail: adding new user (admin, editor or author). If we have HTTP as default protocol for whole website in config.js, all the stuff generated by Ghost will be based on this protocol as a part of base URL - let say http://lnx.azurewebsites.net. This base URL will be also used in e-mails. When You create new user in Ghost, an e-mail with special hash in address is sent to the address You pointed in user's configuration. It looks like:

http://lnx.azurewebsites.net/ghost/signup/MTQzOOk1ODI1OTcyOXxtaXNpZWsxOTI5QGdtYW3sLmNvbXxRcllnL3lMA0dyRkVZY2RVMElHa2V5NW9iSU04YVI5Znd3M3ZMTjZKSUlJPQ/

The first thing is that it will not work with HTTP to HTTPS redirection - You must manually change http to https in link (it's probably caused by wrong regex in rewrite rule).
The second thing is that this request will be sent via insecure channel (http) to the server (Azure) and then redirected (with no success). So if someone is sniffing You, he (or she) can intercept Your sign in URL.

Problem

The problem isn’t obvious at first. Let's analyze Ghost first (briefly). Ghost is based on node.js runtime environment and uses Express web framework. Ghost runs it's own server on 127.0.0.1. User is passing reverse proxy (on Azure it's the IIS webserver) to reach Ghost web application. It is quite standard configuration and there is nothing unusual here except... that Azure proxy does not add the X-Protocol-Proto header. So if You configure Ghost to run on HTTPS it will not work and redirection in IIS via rewrite will be useless.

Solution

Azure Web Apps platform uses open source iisnode (by Tomasz Janczuk) to run Node.js apps. If You install Ghost from Azure Marketplace, in root directory of Your deployment You can find iisnode.yml file. This is a set of iisnode parameters which overwrites iisnode's default configuration. Yes, iisnode is not adding X-Forwarded-Proto header and yes... it can.

The solution is very simple because there is a special parameter You can put into iisnode.yml file to enable adding X-Forwarded-Proto header. The only thing You must do is to add this:

enableXFF: true

You must restart Your Ghost deployment to make it work and remember to change Your Ghost configuration. If You want to have whole site secured, just change url to https. If You want to have only admin panel via HTTPS, You must use forceAdminSSL: true. You also need to remove all HTTP to HTTPS redirection rules from web.config.

The same problem applies to any other webserver as Apache or NGINX (also on Linux). You must remember to properly configure Your webserver to add X-Protocol-Proto header.

Below are examples of configuration:

    production: {
        url: 'http://mysite.azurewebsites.net',

        mail: {
            transport: 'SMTP',
            options: {
                service: 'SendGrid',
                auth: {
                    user: 'azure_x@azure.com',
                    pass: 'pass'
                }
            }
        },
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(__dirname, '/content/data/ghost.db')
            },
            debug: false
        },
        server: {
            // Host to be passed to node's `net.Server#listen()`
            host: '127.0.0.1',
            // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
            port: process.env.PORT
        },
        forceAdminSSL: true
    },

and

    production: {
        url: 'https://mysite.azurewebsites.net',

        mail: {
            transport: 'SMTP',
            options: {
                service: 'SendGrid',
                auth: {
                    user: 'azure_x@azure.com',
                    pass: 'pass'
                }
            }
        },
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(__dirname, '/content/data/ghost.db')
            },
            debug: false
        },
        server: {
            // Host to be passed to node's `net.Server#listen()`
            host: '127.0.0.1',
            // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
            port: process.env.PORT
        },
        forceAdminSSL: false
    },

Default web.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="WEBSITE_NODE_DEFAULT_VERSION" value="0.10.32"/>
  </appSettings>
  <system.webServer>
    <httpErrors existingResponse="PassThrough" />
    <handlers>
      <add name="iisnode" path="index.js" verb="*" modules="iisnode"/>
    </handlers>
    <rewrite>
      <rules>
        <rule name="StaticContent">
          <action type="Rewrite" url="public{REQUEST_URI}"/>
        </rule>
        <rule name="DynamicContent">
          <conditions>
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
          </conditions>
          <action type="Rewrite" url="index.js"/>
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

If You are looking for fresh Ghost distribution for Azure, check Felix Rieseberg's GitHub repo where You can also find one-click deployment option for Azure.

comments powered by Disqus