The recommended way for web traffic is to use HTTPS which requires a certificate. There is a free certificate authority for this purpose, Let's Encrypt.

In this post I describe my configuration and how to automatically renew the certificate when expired. The basic recipe (for Ubuntu 16-04) I followed is available here, another good description is this gist.


I'm using nginx on rpi1, Raspberry Pi Model B Rev 2, running Linux version 4.1.13+ Raspbian GNU/Linux 8 (jessie).

The nginx server works as a proxy-server, redirecting web-application traffic to other raspberries behind this server. My domain name is


Start by cloning the github letsencrypt repository.

$ sudo git clone /opt/letsencrypt

Using the domain name and nginx root directory (see Environment above) we issue the following commands

$ cd /opt/letsencrypt
$ sudo ./letsencrypt-auto certonly -a webroot --webroot-path=/usr/share/nginx/html -d

The script will ask for some questions, be prepared to state email address and accept Terms of Service

I obtained the following response (I was running this 2017-06-26)

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for
Using the webroot path /usr/share/nginx/html for all unmatched domains.
Waiting for verification...
Cleaning up challenges
Generating key (2048 bits): /etc/letsencrypt/keys/0000_key-certbot.pem
Creating CSR: /etc/letsencrypt/csr/0000_csr-certbot.pem

 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/ Your cert
   will expire on 2017-06-26. To obtain a new or tweaked version of
   this certificate in the future, simply run letsencrypt-auto again.
   To non-interactively renew *all* of your certificates, run
   "letsencrypt-auto renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:
   Donating to EFF:          

Let's check the output

$ sudo ls -l -R  /etc/letsencrypt/live/
total 4\
drwxr-xr-x 2 root root 4096 Mar 28 20:53

total 4
lrwxrwxrwx 1 root root  41 Mar 28 20:53 cert.pem -> ../../archive/
lrwxrwxrwx 1 root root  42 Mar 28 20:53 chain.pem -> ../../archive/
lrwxrwxrwx 1 root root  46 Mar 28 20:53 fullchain.pem -> ../../archive/
lrwxrwxrwx 1 root root  44 Mar 28 20:53 privkey.pem -> ../../archive/
-rw-r--r-- 1 root root 543 Mar 28 20:53 README

Next step is to generate a Diffie-Hellman group, this will take some time

$ sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

The output is in the /etc/ssl/certs directory and the file is a container for the public certificate. Now it's time to configure nginx


Let's first define how I have structured the configuration files for nginx. The directory tree for nginx looks like this

├── conf.d
├── sites-available
├── sites-enabled
└── snippets

The basic configuration file is /etc/nginx/nginx.conf, which includes this row (among other) include /etc/nginx/sites-enabled/*;. So all files in the sites-enabled directory is included by nginx. Traditionally, you store configuration files in the sites-available directory, then softlink these files to sites-enabled for easy on/off switching.

So, I have created a configuration file /etc/nginx/sites-enabled/wolfrax.conf. This file defines 2 servers as follows

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

    # This will redirect http traffic to server below using https
    return 301 https://$server_name$request_uri;

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


    root /usr/share/nginx/html;
    index index.html index.htm;

    # SSL configuration
    include snippets/;
    include snippets/ssl-params.conf;

    include snippets/locations.conf;

Let's digest these definitions somewhat.

The 2 listen rows are for TCP/IP v4 and v6 respectively, listening on respective port numbers.

The last row on the first server (return 301 https://$server_name$request_uri;) is important in this context. If the server is approached by a client using HTTP (port 80) it generates a 301-redirect response, this response tells the client where to go (https://$server_name$request_uri) which is simply put the same URI as it first used but using HTTPS instead of HTTP. Thus we enforce usage of HTTPS.


If the target server is upstream and nginx is simply a proxy-pass function the location .well-known needs to be excluded to be upstreamed. See Pelican blog

The second server definition is receiving the HTTPS traffic and includes 3 snippet-files as indicated.

The first file, snippets/, simply includes 2 rows

ssl_certificate /etc/letsencrypt/live/;
ssl_certificate_key /etc/letsencrypt/live/;

These files were generated previously

The second file (snippets/ssl-params.conf) have more information

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;
# Try to use Google ( as resolver
resolver valid=300s;
resolver_timeout 10s;
# 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;

For details on this configuration refer to and Strong SSL Security on nginx

The third file snippets/locations.conf have several upstream locations, what is relevant in this context is this part of the file

location /wolfblog {
    try_files $uri $uri/ $uri/index.html $uri.html @wolfblog;

location @wolfblog {
    # proxy_pass http://rpi2.local:2368; Note, a static IP address makes nginx more robust in case rpi3 is not running
    proxy_redirect    off;
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;

Note that if my domain ( is accessed with this URI:, the first section above kicks in. It tries, in order, to access any files stated in the URI, treat the URI as a directory, access the file index.html in the URI location, and finally and file with extension "html" through the URI. If nothing is found (the normal case) it upstreams to to the @wolfblog location that passes on to the rpi2 node on port 2368 which has a static IP address of

When nginx files have been update, verify the configuration and restart

$ sudo nginx -t
$ sudo systemctl restart nginx

Automatic renewal of certificate

Certificates is valid during a finite time and hence needs to be renewed recurrently. We can do this manually through

$ sudo /opt/letsencrypt/letsencrypt-auto renew

Saving debug log to /var/log/letsencrypt/letsencrypt.log

Processing /etc/letsencrypt/renewal/
Cert not yet due for renewal

The following certs are not due for renewal yet:
  /etc/letsencrypt/live/ (skipped)
No renewals were attempted.

To do this automatically and recurrently, add the following lines into /etc/crontab

30 2    * * 1   root    /opt/letsencrypt/letsencrypt-auto renew >> /var/log/le-renew.log
35 2    * * 1   root    /bin/systemctl reload nginx

This will create a new cron job that will execute the letsencrypt-auto renew command every Monday at 2:30 am, and reload nginx at 2:35am (so the renewed certificate will be used).

Testing SSL configuration

Now try the SSL configuration by pasting this URI into your web browser (exchange for your domain name):