(Web) Serving Raspberry Pi: Part 3 - HTTPS with Let's Encrypt

It's time for part three of my (Web) Serving Raspberry Pi series, in this article we will be looking at encrypting all the things with HTTPS. To do this we use Certbot and Let's Encrypt. However, you will need your own domain in order to fully follow this guide - it's not really something you can just tinker with on your local network. Though you could experiment at home with self-signed certificates, instead of using Let's Encrypt, if you just wanted a HTTPS taster in the safety of your local network.

HTTPS?

As we are setting up a web server I'd like to assume you at least know what HTTP and HTTPS are (and why the latter is desirable), but to cut a (very) long story (very) short: HTTP is the protocol which makes the web work, you see it prefacing URLs all the time, and HTTPS is the secure version of it. To quote Wikipedia: "The main motivation for HTTPS is authentication of the visited website and protection of the privacy and integrity of the exchanged data". These are all good things that we should aim for wherever possible, especially in this age of mass surveillance. To properly enable and use HTTPS you need an SSL Certificate.

Let's Encrypt?

Traditionally obtaining and installing a certificate was a laborious and costly task, most Certificate Authorities (CA) charged a fair amount for their services and did little to aid or encourage none experts. Let's Encrypt makes the process easier and, even better, completely free. In their own words: "Let’s Encrypt is a free, automated, and open Certificate Authority.".

Certbot?

The Electronic Frontier Foundation's (EFF) Certbot works with Let's Encrypt and makes obtaining and renewing certificates much easier than it was previously. There are other tools available for the job, but Certbot is the one Let's Encrypt recommend.

Certbot!

So, we want HTTPS and we are using Certbot with Let's Encrypt because it's easy and it's free. What more reason do you need? :)

Preparation

You will recall from (Web) Serving Raspberry Pi: Part 1 - The Basics that we are doing this on Raspbian Lite, which (at the time of writing) is based on Debian Jesse. However, Debian Stretch is the current "stable" version of Debian (which was released quite recently), while Jesse was first released in 2015. This means that some of the software in Jesse can be a little out of date, or simply isn't there at all.

In our case Certbot isn't included in the Jesse repositories. Though fear not, such things are a common and well documented side effect of using of Debian. While it can be a little more pronounced with Raspbian, as it's release strategy lags a little way behind, it is still not a major problem and is fairly easy to work with or work around. Essentially we have two choices: We can either install Certbot manually or we can add the Debian Jesse Backports repository, which does include Certbot. I prefer to add the backports, because while they aren't considered "stable" they have at least been tested and are known to work.

As previously you will need to run most of the following with root privileges (or just use sudo before each of the commands below):

$ sudo -s

Add Jesse Backports

First edit sources.list:

# nano /etc/apt/sources.list

and add the following line to include the Jesse backports:

deb http://ftp.debian.org/debian jessie-backports main

You will also need to add the appropriate keys, so your Pi can confirm everything is as it should be before allowing you to install packages from the backports repository (more information can be found on the Debian Wiki):

# gpg --keyserver pgpkeys.mit.edu --recv-key 8B48AD6246925553
# gpg -a --export 8B48AD6246925553 | sudo apt-key add -

Install Certbot

With the preparation out of the way, installing Cerbot is a simple task. Though you should run an apt update first:

# apt-get update
# sudo apt-get install certbot -t jessie-backports

Configure/Run Certbot

Configuring and running Certbot is a little more taxing, but still a walk in the park compared to the old way of doing it.

# certbot certonly --webroot -w /path/to/mysite -d mysite -d www.mysite

This will grab a certificate for mysite and www.mysite (you should do both, even if you don't plan to use both), where /path/to/mysite is the path to your site (see (Web) Serving Raspberry Pi: Part 2 - nginx) and mysite is your domain. Once generated your keys and certificates can be found in /etc/letsencrypt/live/mysite. We will get onto what to do with them next in a little while, but first you will need to run this command once for every site you have. Don't forget to adjust the path and site details each time. For example, in my case the command would be:

# certbot certonly --webroot -w /path/to/megalomaniacslair -d megalomaniacslair.co.uk -d www.megalomaniacslair.co.uk

Depending how you have setup your site(s) you may find that you run across some errors at this point. If you have been following this series then you shouldn't have any issues, as we've not done anything likely to cause a problem, yet. However, if you are just here for the Certbot goodness you may hit a snag or two. The Certbot documentation is rather handy, though from experience there is one thing in particular to watch out for. Something many people like to do is limit what is and isn't web accessible (e.g. blocking access to certain directories and/or file types), as a result one quite common issue (and one that got me) is that Certbot needs the hidden directory .well-known to be web accessible. If you do limit access in that way then adding something like this to your site's virtual server configuration (in sites-available) should ensure smooth sailing:

    # Allow certbot access
    location ~ /.well-known {
            allow all;
    }

Renewing Certificates

Once you have a certificate it needs to be renewed regularly (Let's Encrypt certificates expire every 90 days). It is recommended to check if your certificates are due for renewal twice daily, there are numerous ways to do this but my preferred method is to use cron. First edit crontab:

# crontab -e

Then add your new job, for example the following:

14 2,14 * * * certbot renew --renew-hook "systemctl reload nginx"

This runs Certbot renew twice daily at 2:14 and 14:14, it will also reload nginx as necessary. The --renew-hook command only runs if the certificates have been successfully renewed.

nginx

Ok, so now you've got a certificate you need to set nginx up to use it. Start by opening up /etc/nginx/nginx.conf:

# nano /etc/nginx/nginx.conf

and adding:

ssl_session_cache shared:SSL:10m;

This minimises the number of SSL operations run on the server (saving CPU resources) by caching SSL parameters. Next we should generate a new, strong, Diffie-Hellman group:

# openssl dhparam -out /etc/nginx/ssl/dhparams.pem 2048

This will take a while, and you don't have to do this if you don't want. However, while I'm not going to delve into the detail of why it is a good thing here, it is and you should do it. If you want to know more have a read of this Guide to Deploying Diffie-Hellman for TLS.

Next you need to add ssl support to your virtual hosts (sites-available), I use the nginx include directive to include my configuration, that way I can easily mirror the configuration to multiple sites. You can just put it directly in your virtual host configuration if you wish.

First create and edit a file called sslparams.conf (or similar), in a directory readable by nginx. This could be, for example, /etc/nginx/myincludes/.

# nano /etc/nginx/myincludes/sslparams.conf

and add the following:

ssl on;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'
ssl_ecdh_curve secp384r1;
ssl_dhparam /etc/nginx/ssl/dhparams.pem;

It looks like there is an awful lot to take in, but essentially it's just defining the ways browsers can communicate with your server and the levels of security required. The final line is telling nginx to use the Diffie-Hellman group you generated earlier, so if you skipped that bit you can leave the last line off. Those settings will (at the time of writing) achieve an A+ score on the SSLLabs SSL test, which is the de-facto way to benchmark such things. However, it will mean your site won't work with some old and insecure browsers, like IE 6 on Windows XP. That's not a problem for me (in-fact it's desirable) or Google1, but it may be for you. If you desperately need IE6 support then a quick Internet search will help, though a read around the guides on the SSL Labs site (e.g. https://www.ssllabs.com/projects/best-practices/index.html) may convince you otherwise.

Next you want to add all this to site, begin by editing your site's virtual host configuration:

# nano /etc/nginx/sites-available/mysite

Remember the basic configuration from (Web) Serving Raspberry Pi: Part 2 - nginx? Let's alter that as an example:

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;

    # Add index.php to the list if you are using PHP
    index index.html index.htm index.nginx-debian.html;

    server_name mysite www.mysite;

    location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to displaying a 404.
            try_files $uri $uri/ =404;
    }
}

becomes

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    server_name mysite www.mysite;

    return 301 https://$host$request_uri;
}

This redirects HTTP calls to your site (remember mysite is your domain, e.g. megalomaniacslair.co.uk) on port 80 to HTTPS, permanently. Thus any HTTP calls to your site will be automatically redirected to HTTPS before anything else happens. Next we need to setup a new server block to deal with those HTTPS requests, this should go underneath the HTTP server block:

server {
    listen 443 default_server;
    listen [::]:443 default_server;

    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;

    server_name mysite www.mysite;

    # Headers
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Frame-Options "DENY";
    add_header X-Content-Type-Options nosniff;
    add_header Strict-Transport-Security max-age=31536000;

    # SSL
    ssl_certificate /etc/letsencrypt/live/mysite/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mysite/privkey.pem;
    include myincludes/sslparams.conf;

    location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to displaying a 404.
            try_files $uri $uri/ =404;
    }
}

Unlike the previous HTTP server block, this HTTPS block listens on port 443 (the default for HTTPS) and has a few other additions too. Though much of it is pretty much the same as our old HTTP block, the first new addition is the section I've labelled as Headers:

    # Headers
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Frame-Options "DENY";
    add_header X-Content-Type-Options nosniff;
    add_header Strict-Transport-Security max-age=31536000;

These lines add cross-site scripting, clickjacking and mime-sniffing protection, respectively. These are largly optional, but recommended, protections. While the final, and perhaps most important, line enables HTTP Strict Transport Security (HSTS). This is a very good thing that you should definitely activate, even if you don't bother with the rest. Without delving too much into the detail it tells browsers accessing your site to only use HTTPS and helps guard against certain types of attack such as a man-in-the-middle (MITM) attack.

The other new bit adds our previously generated SSL configuration file and certificates:

 # SSL
ssl_certificate /etc/letsencrypt/live/mysite/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mysite/privkey.pem;
include myincludes/sslparams.conf;

Once that's all done you just need to reload (or restart if you prefer) nginx:

# systemctl reload nginx

You should now be able to access your site via https, you might also like to check out your SSL Labs test score: https://www.ssllabs.com/ssltest/.

At this point we are still only serving static HTML files, but at least our connections are now secured via HTTPS. Next time I'll delve into the world of dynamic content, via the Ghost blogging platform. I may touch on PHP too depending on time and the length of what I write about Ghost. Though given Ghost v1.0 just launched in beta form I may well have to make few changes from what I had originally intended.

Next time: (Web) Serving Raspberry Pi: Part 4 - Dynamic content