Let's Encrypt: a Perhaps Decent Tutorial of SSL Encryption for Django, gunicorn, nginx, and Ubuntu Setups

OBS: This tutorial was reviewed and adapted to Ubuntu 16.04 LTS, Ubuntu 17.04, and Ubuntu 17.10 (and derivates). It was not tested in any other operating system. Be careful if not using one of them.
OBS: If you have arrived from a search engine, be mindful it was written several years ago. Tools, pipelines, and commands might have changed over time.


Encryption has suddenly become all the rave, now that Google has finally managed to crack the SHA-1 algorithm. Of course, none of that is going to save your ass from the NSA, so one wonders how essential it really is. It looks like one of those techie causes that every geek decided to start to condone, all of a sudden, like the One Laptop Per Child initiative, or -argh! - the everyone should learn to code. I think Jeff Atwood explains perfectly what I think about it

Well, I'm a self-proclaimed geek and, as such, decided to give in. Of course, it is a nice thing to have, and I'm entitled to have nice things. Plus, it got me a new toy to learn and play with. If there is something I'm always looking for, it's an opportunity to get out of the excruciatingly painful boredom that is my life. I have no idea how I have managed to get readers, but, unless Google Analytics is trying to play an elaborate prank on me, there are apparently about 5 people cringing at reading those lines every day.

This entry will be boring, however. I promised myself I'd post a detailed tutorial of how I achieved that cute green padlock next to my name on your address bar, after dwelling with shitty documentation and incomplete resources for a couple days. Here we go!


All my websites have similar setups because I'm no sysadmin, I'm an engineer and I like automating stuff. They are all virtual private servers (VPS) provided by the folks at Digital Ocean, who, besides being very competent at what they do, were nice enough to throw US$50 (actually US$100, I cheated) at me to experiment their service. If you are a student aged 13 or older, you can get the same deal through GitHub Academic Pack. If not, well, you can squeeze a few bucks out of them using my affiliate link.

I always pick the latest LTS 32-bit Ubuntu version, which, as of today, it's 16.04. Since I'm a dirty Django abuser, my setups always run on the duo gunicorn & nginx. So, from now on, keep in mind this tutorial was made with this four dependencies in mind. If you are running another setup, chances are you'll find a better step-by-step somewhere else.

I will also assume you already have a properly configured domain. If you signed up for the Github Academic Pack, then you get one at the aptly monikered Namecheap for free. This is required since certificates for numeric IP addresses were forbidden a few years ago. 

First steps

I used Let's Encrypt to get a valid SSL certificate. It's quick and easy, once you finally work out the infinite stream of errors it will splash your way, with little to no information for you to analyze. If you are lucky, this tutorial will let you bypass most of them. 

Before anything else, make sure you are logged in as a sudo user, but not as root. I cannot understate how many headaches you will save by doing that. Using the root user will make you create files that gunicorn won't be able to access, and it will throw errors that are difficult to debug. 

I strongly suggest you use the /home/user/ or the /var/www/ directories. If you don't have a sudo user on your machine (or don't have any idea if you do), run:

adduser your_name
usermod -aG sudo your_name
sudo su - your_name

It will require a password from you. Write it down because you are going to use it quite often.

Moving on, Let's Encrypt certificates are free and are compatible with all operating systems and browsers I chose to support. To install it, type:

sudo apt-get update
sudo apt-get install letsencrypt

Before you do anything else, find your Django settings.py file and add these lines:

sudo nano settings.py
# URL that handles encryption files
ENCRYPT_URL = '/.well-known/'
ENCRYPT_ROOT = os.path.join(STATIC_ROOT, '.well-known')

Now, find your urls.py file and add this:

sudo nano urls.py
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ... the rest of your URLconf goes here ...
] + static(settings.ENCRYPT_URL, document_root=settings.ENCRYPT_ROOT)

Next, find the nginx configuration file for your site, should be something like:

sudo nano /etc/nginx/sites-available/yourwebsite.com

Below the server_name yourwebsite.com; line, add this to make nginx allow Let's Encrypt to find your certificates:

location ~ /.well-known { 
root /home/ramonmelo/yourwebsite/static;
allow all;

Run sudo nginx -t to make sure you didn't make any mistake. You should see this appear:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok 
nginx: configuration file /etc/nginx/nginx.conf test is successful

If you get something else, pray to the Church of Google to find a solution. But you have most likely just made a typo, it shouldn't be a big deal. We are not at the difficult part yet.

Restart nginx with the command sudo systemctl restart nginx. Wait a few seconds, then run these commands to tell the firewall to allow HTTP/HTTPS traffic to go through:

sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow 'Nginx-Full'

You should get messages telling Allow rule or Skipped rule, both of them are okay. If you got to this point, you are ready to make NSA's work a little harder. Let's Encrypt some connections, shall we?

Getting certificates

You need to decide where you want to store the encrypted files. It should be in the same folder you told Django. I put mine in the static folder, because it is easier for nginx to serve it, and for Django to find it. Run the following command:

sudo letsencrypt certonly -a webroot --webroot-path=/path/to/website/static -d www.yourwebsite.com -d yourwebsite.com

A weird screen will pop up asking for your email address. After that, it will prompt you to accept the terms of use, just press Agree. This step might take a few minutes, don't get anxious.

This is also the point where most errors should happen. It's impossible to make a guide covering all of them, but, luckily, they are very straightforward. You should be able to handle most of them by yourself, but, if you don't, it's time to go back to the Church of Google. 

Here's a tip: if you are getting 404 errors, go back to your nginx configuration, and to Django settings/urls, and double check them. They must agree with each other, and they must coincide with the actual directory where /.well-known/ is located. 

Another one: 403 errors are either firewall misconfiguration (run ufw status to check it) or a badly configured nginx. Go back there and double check them.

Once you figure everything out and finally get a success message, run:

sudo letsencrypt renew --dry-run --agree-tos

This command attempts to renew your certificate. You should get a message telling you it's too early, and maybe another one telling you need to input your email address. Ignore it, this is a known bug.

The next step will take several minutes to complete, so, if you need a lunch break or some sleep, type it and leave it:

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096

This will generate a random Diffie-Hellman number that will be used to provide you with a secret key. Rumour has it that the NSA has broken into the 1024 bits long keys, so 2048 bits long might be its next target. I have picked 4096 bits long just for peace of mind. Don't trust me on this, though, it's probably innocuous. Let's be honest: if they really wanted to, NSA could crack this blog in a matter of seconds. The only reason for me to do this is the shiny padlock badge next to my name.

Once that command finishes, your website should have properly encrypted communications. It is still not enough, however. We have some patching up to do now.


Create the following file and add those two lines in it:

sudo nano /etc/nginx/snippets/ssl-www.yourwebsite.com.conf
ssl_certificate /etc/letsencrypt/live/www.yourwebsite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.yourwebsite.com/privkey.pem;

Make sure those two files actually exist. You might need to fix their names on the file above.

If you are sure they exist, in that file path, then create this new file:

sudo nano /etc/nginx/snippets/ssl-params.conf

And paste this configuration that I have shamelessly copied from somewhere else:

# from https://cipherli.st/
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver valid=300s;
resolver_timeout 5s;
# Disable preloading HSTS for now.  You can use the commented out header line that includes
# the "preload" directive if you understand the implications.
#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

ssl_dhparam /etc/ssl/certs/dhparam.pem;

That is a basic security configuration that should be enough for most websites. I won't even pretend I understand what most of those code lines mean, nor should you. Just know that, unlike you and me, the professionals that typed that code are actually competent. The earlier you accept it as a fact, the earlier you can move on.

We are going to make changes to your nginx configuration (again). These might break nginx for good, so common sense tells us to backup the file:

sudo cp /etc/nginx/sites-available/yourwebsite /etc/nginx/sites-available/yourwebsite.bak

Now that the certificates have been successfully emitted, it is suggested that you avoid using unencrypted traffic altogether. Of course, your users, being even more stupid than you, have no idea of how to accomplish that, so you need to strip away wrong choices from them. We are going to edit this file:

sudo nano /etc/nginx/sites-available/yourwebsite

To effectively disable the port 80 (regular HTTP). Every request, from now on, should be handled only through the port 443 (HTTPS, with S as in Secure). Edit the existent entries in there with these lines:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name yourwebsite.com www.yourwebsite.com;
    return 301 https://$server_name$request_uri;

server {

    # SSL configuration
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;
    include snippets/ssl-www.yourwebsite.com.conf;
    include snippets/ssl-params.conf;

# other stuff

As usual, run sudo nginx -t right after to make sure you didn't fuck it up. You should get this message:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If you received a complaint about default_server, this means that you need to get rid of the default nginx configuration file:

sudo mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak

Maybe get rid wasn't an adequate term, you just renamed it so nginx couldn't find it anymore. Let this phylosophical conclusion sink in: you don't need to get rid of your problems, you just need to be able to never find them again.

Once you recover from this egregious attempt at smart humor, restart nginx

sudo systemctl restart nginx

Wait a full minute, then open your favorite web browser and check this website: https://www.ssllabs.com/ssltest/analyze.html?d=yourwebsite.com. Make sure you replaced the placeholder with your actual website address. This is a test of your SSL implementation, it should return you an A+ after a few minutes, be patient. 

After the result, your website is fully encrypted and ready to go. In order to safely stay this way, let's set a cron job (geeky slang for a scheduled task) that will automatically renew your certificate. Let's Encrypt suggests two jobs as a safety measure:

sudo nano /etc/cron.monthly/renew1.sh
sudo nano /etc/cron.monthly/renew2.sh

In both files, copy and paste this same script, and let Ubuntu do its magic:

sudo letsencrypt renew
sudo systemctl reload nginx

Finally, as an additional layer of security, let's also teach Django how to behave under HTTPS. No more bash scripts, however, this time we'll do it the Python way. First, make sure your virtual environment is active. From the same directory where you found your settings.py and urls.py files:

source ../venv/bin/activate

A (venv) or similar must have shown up in the terminal. If so, run:

pip install django-sslify

This installation takes a couple minutes. Once it's done, open your settings.py file:

sudo nano settings.py

Look for the MIDDLEWARE_CLASSES variable. Once you find it, type in there, right in the first line:

MIDDLEWARE_CLASSES = ( 'sslify.middleware.SSLifyMiddleware',
# ... other stuff ...

That's it. You have done it. It is a pain in the ass in the first time, but it gets remarkably easier the next time. I just wish there was a tutorial with this setup when I was deploying my first website. But I guess I can't complain that much. After all, it has provided me with the opportunity to share my knowledge with whomever is stupid enough to pay attention to me, and also has kept this wheel spinning. This is the third blog post I have written in 48 hours, which is something I would have never thought possible. 

Last thoughts

I'm not going to lie: maintaining systems is boring as fuck. I love coding, though, and setting up the adequate environment for my apps to perform is a requisite, unfortunately. I do miss my starting days, spending nights between heavy metal songs and liters of coffee, coding useless Facebook apps on Heroku that never saw the light of day, because deployment was never worrisome. 

Heroku apps can get expensive quite fast, though. In the end, the perks of having cheap scalability and full infrastructure control far outweigh the disadvantages of having extra responsibilities. VPSs do make the job a lot more manageable, however. I gotta say that, if I couldn't preconfigure and build my environments using a foolproof web GUI and then repeatedly clone and adapt them in loco to suit my needs, I'd still be corralled on Heroku, fighting to fit into those measly 10K database rows the free plan would allow me. One of these days, I'll honor my promises and debunk that stupid myth of IaaS and PaaS are two different services that can't be compared

At this point, I actually have a bash script that does most of the typing for me, so it doesn't bother me as much. I'm still reliant on the Django/gunicorn/nginx/Ubuntu stack, I must confess, but that's a ludicrously easy setup to reproduce elsewhere. I might try it the next time I find myself drowning in the never-ending boredom. Until then, I hope the workflow I have just delineated here remains useful for a long time and helps someone else overcome the fear of deploying their own apps to the cloud. Encryption itself does not mean much, and this process is quite annoying to deal with, but it has to be done only once, and it enables web applications that require logins and passwords, effectively unlocking another set of features for developers.

Or whatever else I need to say to convince myself that the job was done and I deserve some sleep. 

Tags: tools linux server 



There are currently no comments

New Comment