How to Heroku: force https connections
Written by Allard Stijnman on 5th March 2015
If, like me, you want all the connections to your Heroku app to be secure, and you’re using django + gunicorn, here’s how to do it. There are more ways to do this, but this is how it worked for me.
Identifying the problem
So you’ve added an SSL add-on or you’re just using the piggyback certificate provided by Heroku, but you can still access your app through insecure connections. What gives?
Lack of documentation
What I found the most frustrating was that this is a largely undocumented issue. Both the Heroku and gunicorn docs (latest versions) are lacking this. There are plenty of StackOverflow questions about this, all with good answers, just missing one vital step.
So here we go, combining what I’ve learned with things from other posts. It’s actually really easy when you know how.
Heroku router
The heroku router is a large part of the initial problem, it strips the request of all information regarding the used protocol (http vs https). Lucky for us however, it adds a new header to replace this. Now all we’ve got to do is get our app to listen to thisX-FORWARDED-PROTO
header.
Gunicorn config
We will do this by adding a gunicorn configuration file (if you don’t already have one). In this file we add the lines:
forwarded_allow_ips = '*' secure_scheme_headers = { 'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on' }
Basically the first line is telling gunicorn we are in a trusted environment and we should accept these headers from all incoming ip adresses. We need to do this, because we don’t know the Heroku routers ip in advance. This was the hard part, I didn’t see any other posts refer to this, and it was removed in later versions of gunicorn docs.
The second line is telling gunicorn which headers to look for and what content they should have in order to be considered secure. As you might have noticed, this part was freely available all over StackOverflow.
Now change your Procfile to say the following:
gunicorn --config=your/gunicorn/config.py yourproject.wsgi:application
NB: Don’t forget to add your worker configuration to this file as well, amongst other things! For a full reference check out the gunicorn docs.
NB2: You can also do this using Django’s settings, but I figured gunicorn would be more suitable for this kind of stuff.
Django config
Because we’re not running nginx or apache in front of our application which can do the redirecting part, we need to do this manually. I haven’t found how to do this using a standard Heroku add-on or using gunicorn, so we’ll be doing it in Django using some middleware. Fortunately for us there is someone who’s already made this into a pip installable package.
Just add that to the INSTALLED_APPS
and MIDDLEWARE_CLASSES
in your Django settings. By default it looks at your DEBUG
setting to determine whether to redirect or not, so that’s the only configuration needed.
Your thoughts
Written by Matt Woodward on 5th April 2016
With the new SECURE_SSL_REDIRECT setting as of Django 1.8 is it still necessary to use django-sslify on Heroku? I’ve been doing some testing today and the built-in Django settings work in a local test environment (Nginx + gunicorn) but on Heroku it doesn’t ever do the redirect, so I’m wondering if django-sslify is required on Heroku to get it working. Thanks.
Written by Allard Stijnman on 6th April 2016
We don’t use that setting yet, but I’ve looked at the code on github and I would say you wouldn’t have to use django-sslify anymore. So my guess is that you have other settings that are the issue here. The redirect check looks at three things:
– Have you set `SECURE_SSL_REDIRECT` to `True`.
– Is the current request not https.
– Is the current page exempted from https redirects.
Point 1 and 3 are easy to check, but point two can be tricky because of the headers. You mentioned you also use nginx, do you have that set to forward headers? We use https://github.com/KazW/nginx-buildpack which forwards some headers:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
To debug I would suggest copying the django middleware in a custom one and insert logging statements to determine which part of the if check is failing on heroku.