Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save davewongillies/6897161 to your computer and use it in GitHub Desktop.
Save davewongillies/6897161 to your computer and use it in GitHub Desktop.
Serving Django apps behind SSL with Nginx

Configuring Nginx to serve SSL content is straight forward, once you have your certificate and key ready:

server { 
    listen 443 default ssl;
    root /path/to/source;
    server_name mydomain;

    ssl_certificate      /path/to/cert;
    ssl_certificate_key  /path/to/key;


    client_max_body_size 10M;
    access_log /var/log/nginx/alog.log;
    error_log /var/log/nginx/elog.log;


    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass  http://unix:/sourcepath/run/app.sock;
    }

}

But you will find, as I did, that Django makes redirections from https to http URLs when you use an HttpResponseRedirect object.

When Django does a redirection, it composes an absolute URI based on the one in the request object. And the request object uses its method build_absolute_uri() to make this URI, which in turn calls another HttpRequest method, is_secure(), to check whether to use http or https.

OK, that was all an intro. The interesting bit I want to share is that this is_secure() method works differently under Django 1.3 or 1.4. Under 1.3 it only checks whether an environment variable named "https" exists and its value is "on":

def is_secure(self):
        return os.environ.get("HTTPS") == "on"

So if you want to make sure you serve secure URLs all the way, you need to setup that environment var.

But under 1.4 it does another check before doing that one. It looks for the HTTP header X-Forwarded-Proto, and if it is present, it returns True (https request). This header has to be added to in the Nginx configuration for the secure server so it is passed to gunicorn:

location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_set_header X-Forwarded-Proto $scheme
        proxy_pass  http://unix:/sourcepath/run/app.sock;
    }

NB X-Forwarded-Proto

I've wasted hours of my life trying to figure out why X-Forwarded-Protocol didn't work. Well its because at some point in time, either it worked or the collective consciousness of the Internet got it wrong and perpetuated this incorrect information. The correct proxy_set_header is in fact X-Forwarded-Proto. Hopefully this tidbit will save you from wasting hours of your life like I did.

@Remiz
Copy link

Remiz commented Jun 3, 2016

Thanks, you did prevent me to waste hours. I had X-Forwarded-Protocol in all my configs... And I was wondering why it kept redirecting in a loop. Well, the good news is that your gist starts to rank well on Google for the right keywords :). Thanks again.

@Giribhushan
Copy link

Giribhushan commented Aug 29, 2016

I get following error when I tried to add proxy headers to my vagrant local host nginx.conf file.
"upstream prematurely closed connection while reading response header from upstream, client: 10.0.2.2, server:"
How do I resolve it ? I am confused on the value for proxy_pass.

I keep getting 502 Bad Gateway error for nginx/1.11.1.

@lewiscollard
Copy link

Hey davewongillies,

Late to the party here, but I think you are missing a semicolon on the end of your proxy_set_header line. :) Thank you for saving a lot of people a lot of time, me included!

@dmarcelino
Copy link

Thanks @davewongillies!

@rivernews
Copy link

Big thanks for the great explanation @davewongillies! Even if it's 3 years ago it still has been very useful.
If anyone is coming over because of Django REST Framework pagination link http problem, you also have to set SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') in your settings.py.

The problem that cause my issue is a bit different: I have two duplicated proxy_set_header X-Forwarded-Proto $scheme; in my nginx.conf. I'm using Kubernetes official nginx ingress controller, and turns out they already have proxy_set_header X-Forwarded-Proto $scheme; by default, perhaps that is a popular configuration. So when I add a location-snippet to add that line, I ended up having two of them. I couldn't figure this out until I print out all the headers by request.META in Django and have a "gotcha" moment after seeing 'HTTP_X_FORWARDED_PROTO': 'http,https'. What I've learned from this is proxy_set_header is not a "set", should be more like "append" instead. Ah, what a waste of a week.

@ClementGautier
Copy link

@rivernews: I'm in the EXACT SAME situation, I can't get Django to build the urls well because I have the header like this 'HTTP_X_FORWARDED_PROTO': 'http,https' because I use nginx-ingress and can't rely only on use-forwarded-headers so I ended up using a snippet too. Anyway: what was your solution? Did you used a custom header instead?

@rivernews
Copy link

rivernews commented Jan 21, 2020

@clemen

@rivernews: I'm in the EXACT SAME situation, I can't get Django to build the urls well because I have the header like this 'HTTP_X_FORWARDED_PROTO': 'http,https' because I use nginx-ingress and can't rely only on use-forwarded-headers so I ended up using a snippet too. Anyway: what was your solution? Did you used a custom header instead?

I just removed my location-snippet at the end. In your case -- I think it depends on your ingress setup.

My K8 nginx controller is directly exposed to external world. I did not use any cloud provider's load balancer, therefore the default nginx-ingress forward headers will give me the correct http/https scheme. Nginx will set the correct scheme into $scheme, so we'll use it instead of other insecure custom header that could be possibly altered by client.

I wrote a StackOverflow answer to this - one thing may be worth mentioning is there's a guy commenting below using AWS ALB. Since his ALB config directs all traffic to http internally, he had to use a different header which is provided by AWS ALB, in order to get the correct scheme. I guess in case you are using a load balancer in front of nginx ingress, his situation might be helpful for you?

@ClementGautier
Copy link

@rivernews: thx for the follow up :D In my case I ended up using a custon header (X-Forwarded-Proto-Custom) and setting SECURE_PROXY_SSL_HEADER to read this custom header instead while I wait for the provider that deliver the first layer of Reverse Proxy to actually forward the headers needed. In your case you are right, the default headers should be alright without additional configuration ;)

@rivernews
Copy link

@clemen glad to help!

@mexomagno
Copy link

Came here just to deeply thank all of you guys, thanks to your hours of pain I was able to solve this in 5 minutes ✨

@spirovskib
Copy link

@davewongillies I would ask for help on my specific configuration:
I have a Django application executed by gunicorn with an Nginx reverse proxy. Everything runs on the same Docker instance, run by Supervisord. Gunicorn runs on port 8000, Nginx runs on port 80.

The Nginx config is

server {
    listen      80;

    location /static/ {
        alias /home/app/static/static_assets/;
    }

    location /media-files/ {
        internal; # only the django instance can access this url directly
        alias /home/app/media/media_assets/;
    }

    location / {
        proxy_pass http://localhost:8000;
    }
}

I am trying to use the above solution to make OAuth2 work with Google. With the original configuration the error is as follows:

Error 400: redirect_uri_mismatch
The redirect URI in the request, http://localhost:8000/accounts/google/login/callback/, does not match the ones authorized for the OAuth client. To update the authorized redirect URIs, visit: https://console.developers.google.com/apis/credentials/oauthclient/${your_client_id}?project=${your_project_number}

If I add the configuration snippet I get the error below:

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_set_header X-Forwarded-Proto $scheme;

Code: unknown, Error: HTTPSConnectionPool(host='accounts.google.com', port=443): Max retries exceeded with url: /o/oauth2/token (Caused by ProxyError('Cannot connect to proxy.', NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f79207eb640>: Failed to establish a new connection: [Errno -2] Name or service not known')))

@nicam
Copy link

nicam commented Dec 21, 2020

@rivernews Thanks I had the exact same issue! so with Kubernetes, actually NOT setting:
proxy_set_header X-Forwarded-Proto

in nginx was the solution, as kubernetes already sets this to https

@pipchris
Copy link

pipchris commented Aug 5, 2021

thanks. your docs save my time alot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment