How to deploy an HTTPS-only site, with Django/nginx?
Asked Answered
C

3

44

My original question was how to enable HTTPS for a Django login page, and the only response, recommended that I - make the entire site as HTTPS-only.

Given that I'm using Django 1.3 and nginx, what's the correct way to make a site HTTPS-only?

The one response mentioned a middleware solution, but had the caveat:

Django can't perform a SSL redirect while maintaining POST data. Please structure your views so that redirects only occur during GETs.

A question on Server Fault about nginx rewriting to https, also mentioned problems with POSTs losing data, and I'm not familiar enough with nginx to determine how well the solution works.

And EFF's recommendation to go HTTPS-only, notes that:

The application must set the Secure attribute on the cookie when setting it. This attribute instructs the browser to send the cookie only over secure (HTTPS) transport, never insecure (HTTP).

Do apps like Django-auth have the ability to set cookies as Secure? Or do I have to write more middleware?

So, what is the best way to configure the combination of Django/nginx to implement HTTPS-only, in terms of:

  • security
  • preservation of POST data
  • cookies handled properly
  • interaction with other Django apps (such as Django-auth), works properly
  • any other issues I'm not aware of :)

Edit - another issue I just discovered, while testing multiple browsers. Say I have the URL https://mysite.com/search/, which has a search form/button. I click the button, process the form in Django as usual, and do a Django HttpResponseRedirect to http://mysite.com/search?results="foo". Nginx redirects that to https://mysite.com/search?results="foo", as desired.

However - Opera has a visible flash when the redirection happens. And it happens every search, even for the same search term (I guess https really doesn't cache :) Worse, when I test it in IE, I first get the message:

You are about to be redirected to a connection that is not secure - continue?

After clicking "yes", this is immediately followed by:

You are about to view pages over a secure connection - continue?

Although the second IE warning has an option to turn it off - the first warning does not, so every time someone does a search and gets redirected to a results page, they get at least one warning message.

Connivance answered 16/11, 2011 at 15:9 Comment(0)
T
65

For the 2nd part of John C's answer, and Django 1.4+...

Instead of extending HttpResponseRedirect, you can change the request.scheme to https. Because Django is behind Nginx's reverse proxy, it doesn't know the original request was secure.

In your Django settings, set the SECURE_PROXY_SSL_HEADER setting:

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

Then, you need Nginx to set the custom header in the reverse proxy. In the Nginx site settings:

location / {
    # ... 
    proxy_set_header X-Forwarded-Proto $scheme;
}

This way request.scheme == 'https' and request.is_secure() returns True. request.build_absolute_uri() returns https://... and so on...

Twostep answered 28/10, 2013 at 14:18 Comment(5)
Does it matter that django's docs seems to suggest HTTP_X_FORWARDED_PROTO rather than HTTP_X_FORWARDED_PROTOCOL?Consistency
Both would work (and any other name), as long as you set the same header in nginx and Django settings (X-Forwarded-Proto in nginx). I wasn't aware that "proto" is the convention when writing this...Twostep
I updated the answer to be consistent with the docs, thanks for pointing it out.Twostep
For anyone serving django with nginx and uwsgi. Note that you don't need to specify 'SECURE_PROXY_SSL_HEADER' as in this case django already knows whether the request is secure or not. is_secure() is checking the scheme property, which invokes _get_scheme(self), which in turn should be implemented by WSGIRequest, which does return self.environ.get('wsgi.url_scheme'). And -- you guessed it -- this environment variable is filled by uwsgi accordingly.Bombycid
if you are using AWS ALB/ELB to handle http/https redirection, and are sending http request to your nginx containers, then set the header as proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;Staff
C
21

Here is the solution I've worked out so far. There are two parts, configuring nginx, and writing code for Django. The nginx part handles external requests, redirecting http pages to https, and the Django code handles internal URL generation that has an http prefix. (At least, those resulting from a HttpResponseRedirect()). Combined, it seems to work well - as far as I can tell, the client browser never sees an http page that the users didn't type in themselves.

Part one, nginx configuration

# nginx.conf
# Redirects any requests on port 80 (http) to https:
server {
    listen       80;
    server_name  www.mysite.com mysite.com;
    rewrite ^ https://mysite.com$request_uri? permanent;
#    rewrite ^ https://mysite.com$uri permanent; # also works
}
# django pass-thru via uWSGI, only from https requests:
server {
    listen       443;
    ssl          on;
    ssl_certificate        /etc/ssl/certs/mysite.com.chain.crt;
    ssl_certificate_key    /etc/ssl/private/mysite.com.key;

    server_name  mysite.com;
    location / {
        uwsgi_pass 127.0.0.1:8088;
        include uwsgi_params;
    }
}

Part two A, various secure cookie settings, from settings.py

SERVER_TYPE = "DEV"
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True # currently only in Dev branch of Django.
SESSION_EXPIRE_AT_BROWSER_CLOSE = True

Part two B, Django code

# mysite.utilities.decorators.py
import settings

def HTTPS_Response(request, URL):
    if settings.SERVER_TYPE == "DEV":
        new_URL = URL
    else:
        absolute_URL = request.build_absolute_uri(URL)
        new_URL = "https%s" % absolute_URL[4:]
    return HttpResponseRedirect(new_URL)

# views.py

def show_items(request):
    if request.method == 'POST':
        newURL = handle_post(request)
        return HTTPS_Response(request, newURL) # replaces HttpResponseRedirect()
    else: # request.method == 'GET'
        theForm = handle_get(request)
    csrfContext = RequestContext(request, {'theForm': theForm,})
    return render_to_response('item-search.html', csrfContext)

def handle_post(request):
    URL = reverse('item-found') # name of view in urls.py
    item = request.REQUEST.get('item')
    full_URL = '%s?item=%s' % (URL, item)
    return full_URL

Note that it is possible to re-write HTTPS_Response() as a decorator. The advantage would be - not having to go through all your code and replace HttpResponseRedirect(). The disadvantage - you'd have to put the decorator in front of HttpResponseRedirect(), which is in Django at django.http.__init__.py. I didn't want to modify Django's code, but that's up to you - it's certainly one option.

Connivance answered 18/11, 2011 at 14:46 Comment(1)
There is no need to add certificates for Django, for it to be able to send back secure cookies ? I guess then it assumes connection is secure if nginx only responds to HTTPS ?Nunci
P
5

if you stick your entire site behind https, you don't need to worry about it on the django end. (assuming you don't need to protect your data between nginx and django, only between users and your server)

Polynesian answered 16/11, 2011 at 16:14 Comment(4)
Are you saying use nginx to do all the configuration?Connivance
all you need to do is set nginx to only respond to https requests (possibly redirecting any non-https to https). django can just run over http since it's only talking to nginx on localhostPolynesian
I configured nginx to redirect everything to https, but it hasn't worked fully - redirecting to another URL with variables, as https://mysite.com/search?results="abc", drops the variables.Connivance
Then @Polynesian there is no need, according to you, to send cookies over HTTPS only ? (secure cookies).Nunci

© 2022 - 2024 — McMap. All rights reserved.