Ever since Snowden revealed that the NSA is spying on all of us the urge to more secure systems increased. At least over in Germany and plenty other European countries.
There’s also the risk of public and unencrypted Wi-Fis. You don’t want the owner of the cafe next door or the other people sitting in the cafe be able to wiretap your network traffic.
And some internet service providers even inject their own advertisement in websites you access. That’s scary. And a bad idea from a user’s perspective. Not to mention from a security perspective.
I’m not a cryptographer. All examples I give here are either to the best of my knowledge or I’ve talked to skilled people I trust and asked them for their knowledge. Which still means there can be errors as we all a humans!
This blog post will not cover everything you could possibly know about SSL and TLS, by all means, that would be enough to fill an entire conference with talks.
Also, SSL 2 and 3 are broken and insecure. Do not use them! Period. TLS 1.0 and 1.1 are discouraged and superseded and should also not be used anymore as well. Unless you really need to support old browsers.
SSL stands for Secure Socket Layer. TLS stands for Transport Layer Security. But that’s just words. They are both cryptographic protocols for communication systems most notably networks. Either one provides 2 of the 3 parts of “Information security”:
Availability is the third part in Information Security but needs to be handled on a different layer.
Because this is the blog post for my talk from DjangoCon US 2016 and most things we do with Django these days are website, let us have a look at how to configure web servers to handle SSL.
Let’s start with Apache 2 or httpd
<VirtualHost *:443>
ServerName example.com
SSLEngine on
# Details at https://cipherli.st/
SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH
SSLHonorCipherOrder on
SSLProtocol all -SSLv3
SSLCertificateFile /etc/nginx/ssl/example.com.crt
SSLCertificateKeyFile /etc/nginx/ssl/example.com.key
SSLOpenSSLConfCmd DHParameters "/etc/nginx/ssl/example.com.dh"
</VirtualHost>
You need to switch on the SSL engine. You want to define which ciphers you want and make sure the connection is going to use the best cipher it can, according to your list, not the client’s.
You also want to ensure to not use SSL and only TLS and maybe go for TLS 1.2 only.
Defining a custom Diffie-Hellman Parameter is also a good idea. That is used for the initial key exchange between a client and a server.
And this is the syntax for Nginx. It looks pretty much the same as the one for Apache 2.
host {
listen [::]:443 ssl;
server_name example.com;
# Details at https://cipherli.st/
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_dhparam /etc/nginx/ssl/example.com.dh;
}
Now that we know how to configure web servers where the heck do we get the certificates from?
And what the heck is “Let’s Encrypt“?
And how does the whole SSL certificate thing work anyway??
In order to answer these questions I need to tell you a bit about how the entire certificate machinery works.
Let us start with CAs: CA stands for Certificate Authority. These are “trusted” entities we or our browsers rely on in order to establish a chain of trust
Browsers, email clients, and other programs have a pre-installed set of root certificates. These are provided by Root CAs, CAs that e.g. browser vendors — such as Mozilla and Google — trust. That database of root certificates is the “trust store”.
The Root CAs sign intermediate certificates. Most Root CAs just sign their own intermediate certificates. Some Root CAs though (in the diagram “Root CA 3”) are not in the trust store. So they are not automatically trusted by our browsers.
Trusted Root CAs can cross sign other CA’s intermediate certificates and make certificates signed by “Intermediate CA 3” trusted by a browser.
Which brings me to the next point: the certificate you can buy from well known vendors are not signed by their root certificates but are signed by the intermediate certificates.
As a result, when your browser trusts Root Cert 1, it trusts Intermediate Cert 1, and hence it trusts all certificates signed by Intermediate Cert 1.
If your browser trusts Root Cert 2, it trusts Intermediate Cert 3, and hence it trusts all certificates signed by Intermediate Cert 3.
It’s a Root CA, that’s not in global trust stores. At least not yet. Their Intermediate certificate is cross-signed by IdenTrust which is in all common trust stores.
But Let’s Encrypt has control over their intermediate Cert, which allows them to sign arbitrary other certificates, which we will use for our servers.
Let’s Encrypt offers an API to get “unlimited” SSL certificates for your domains, free of charge.
By now they issued more than 5 million certificates through their API.
The process during which one can get a certificate is defined as ACME or Automatic Certificate Management Environment.
It’s a fairly simple JSON API with some crypto magic separated in 4 major steps.
In order to use the Let’s Encrypt API one needs three things:
The first API endpoint is “new-reg”. This authenticates you as a “person” or “server” against Let’s Encrypt. You send your Account Key’s public key signed with the private key to Let’s Encrypt. You can optionally include a mail address to get notified about expired certificates.
Second, there is “new-authz”. You send your CSR to Let’s Encrypt signed with your Account Key. The API responds with a list of challenges. These are specially crafted URLs, with specific content, under each requested domain, you need to make available. This is the step to proof you have control over the content served under a given domain.
“challenge” is the next step. For each challenge you received, Let’s Encrypt is going to request the related challenge you made available and check for its correctness.
Lastly, “new-cert”. When all challenges succeeded you can request the certificate and put that in the place where your web server finds it.
And here’s the Apache 2 / httpd config to make those challenges available
<VirtualHost *:80>
ServerName example.com
Redirect / https://example.com/
Alias "/.well-known/acme-challenge/" "/srv/http/acme-challenges/"
<Directory "/srv/http/acme-challenges">
AllowOverride None
Options None
Require all granted
</Directory>
</VirtualHost>
And the same one for Nginx.
host {
listen [::]:80;
server_name example.com;
location /.well-known/acme-challenge/ {
alias /srv/http/acme-challenges/;
try_files $uri =404;
}
location / {
return 301 https://example.com$request_uri;
}
}
There’s an official client which does “all” the magic. Literally, it rewrites your Apache config file.
That’s the wrong approach, in my opinion. That’s a task for configuration and system management tools. Watch my talk from 2015th PyCon Australia for details on that topic.
There’s a script acme-tiny by Daniel Roesler which has 200 lines. Easy to understand. I Recommended to read through the code. I forked that and added support for Systemd and a few other things.
There are also a bunch of other tools. E.g. letsencrypt-aws by Alex Gaynor. I haven’t used it myself, but it probably just does what the name suggests.
And there’s Rproxy by Amber Brown
Personally and at djangoproject.com we use my fork of acme-tiny.
But all the scripts boil down to the same things: You need an account key, a certificate signing request, a place to put the challenges, and a final location for the full certificate.
$ python3 /etc/acme-tiny/acme-tiny.py \
--account-key "/etc/acme-tiny/account.key" \
--csr "/etc/acme-tiny/example.com.csr" \
--acme-dir "/srv/www/acme-challenges" \
--output "/etc/nginx/ssl/example.com.crt" \
--combine "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem"
Now that this is sorted, how do we use HTTPS in Django and what do you need to change?
As it turns out, not much.
You want to make cookies only accessible via HTTPS. But only on production systems. You can’t use HTTPS locally at least not without some complex steps. At least not with built-in features. There is, however, a runserver_plus command in the django-extensions 3rd party app. Hence, set the settings variables through environment variables:
import os
CSRF_COOKIE_SECURE = os.getenv('SECURE_COOKIES') == 'yes'
SESSION_COOKIE_SECURE = os.getenv('SECURE_COOKIES') == 'yes'
If you use secure cookies with the runserver command this might manifest as a HTTP 403 error when you try to login to the admin because the CSRF cookie won’t be submitted.
Apart from that, there a couple of things you can optionally enable. Have a look at the Security Topic in the Documentation.
Most things you don’t want to do in Django though, but in your reverse proxy if at all possible. It’ll be faster.
Let’s Encrypt’s certificates are only valid for 90 days. If you need to revoke one before, there’s an API call for that.
If you lost your account key or it was compromised you can change it as well.
You probably want HSTS (HTTP Strict Transport Security). Really. Running a website on HTTP and HTTPS may make it possible for attackers to force HTTP requests.
Similar to HSTS there’s also HPKP (HTTP Public Key Pinning).
This feature is neat but mostly not too useful unless you’re a big organization and you are regular victim of DNS attacks.
And of course you can use the Let’s Encrypy certificates for services other than HTTPS. The only thing you need to make sure is that the domains for that certificate serve valid challenge responses on port 80 as explained above.
And because we’re talking about cryptographic tropic, here’s an incomplete list of things that can go wrong: