SSL in Nginx using Let’s Encrypt

When I saw the first news of the Let’s Encrypt project several months ago I got pretty excited about it, mostly because I don’t like to use self-signed certificates and this blog didn’t seem worthwhile to pay for a proper certificate on.  Combined with the recent news of Google starting to shame sites not using SSL I figured I would give it a shot.  Here is a quick rundown of how I configured my nginx web server to use Let’s Encrypt SSL certificates.  The official documentation for Let’s Encrypt can be found here.

A few things to note at the start:

  • I did not use the nginx plugin, as that is very broken
  • I followed a fairly manual process using the “webroot” method thanks to this tutorial
  • My Linux distro is Arch Linux

This first thing you need to do is install/configure Let’s Encrypt. You can do this in any number of ways but the easiest way on Arch Linux is to install it via pacman

# pacman -Sy letsencrypt

You don’t need the optional packages as we won’t be using them. At this time of this writing the nginx plugin is pretty much completely broken.

The webroot method creates files in a hidden directory that the LE servers check against for validation and certificate generation.

My original Nginx configuration consisted of a default catch-all site listening on port 80 which would serve a static page to any domain not found with a valid vhost.  All of my site vhosts are configured in various config files separately.  To enable my server to have a nice integration with Let’s Encrypt I created a new configuration for the domains I want certificates for to listen on port 80 to enable the “acme-challenge” response for the domains I want certificates for.  For domains I do not want/need certificates for, they can still be served on port 80.

The configuration file I created for the acme-challenge webroot looks like this:

server {
        listen                  80;
        listen                  [::]:80;
        server_name             .example.com;

        location '/.well-known/acme-challenge' {
                default_type    "text/plain";
                root            /tmp/letsencrypt-auto;
        }

        location / {
                return          301 https://$server_name$request_uri;
        }
}

So this says that for the domains I want caught in this certificate, which all redirect to this site, respond to the LE “acme-challenge” on port 80, otherwise 301 redirect them to the SSL version of the site.  This enables easy renewal of the certificates.  You must restart nginx to reload the configuration.

Once I have nginx set up to respond in this manner I generated the certificates using a simple script (again, thanks to renchap):

#!/bin/bash -x

DIR="/tmp/letsencrypt-auto"
DOMAINS="-d example.com -d www.example.com"

mkdir -p $DIR && letsencrypt certonly --server https://acme-v01.api.letsencrypt.org/directory -a webroot --webroot-path=$DIR --agree-tos $DOMAINS

In this script the DIR variable points to a temporary location LE will use to challenge against, as per the nginx config file above, the DOMAINS variable is a list of all the domains you want included in this certificate chain.

I then reconfigured this particular vhost to now be set up for SSL, the relevant lines to add/change in the server block are:

listen          443 ssl;
server_name     .example.com;

ssl_certificate         /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key     /etc/letsencrypt/live/example.com/privkey.pem;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';

At this point you can restart nginx and you should be good to go.

Let’s Encrypt certificates must be renewed every 90 days, so I set up a systemd service and timer to handle the renewal automatically.

I made a simple shell script stored in /usr/local/letsencrypt.sh as follows:

#!/bin/bash -x

DIR="/tmp/letsencrypt-auto"
DOMAINS="-d example.com -d www.example.com"

mkdir -p $DIR && letsencrypt --renew certonly --server https://acme-v01.api.letsencrypt.org/directory -a webroot --webroot-path=$DIR --agree-tos $DOMAINS

systemctl restart nginx

Then, in /etc/systemd/system I created a service and timer file:

[Unit]
Description=Renew Let's Encrypt certificate monthly https://letsencrypt.readthedocs.org/en/latest/using.html#renewal
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/letsencrypt.sh

You can verify this service will work by manually triggering it (for example: systemctl start letsencrypt)

[Unit]
Description=Renew Let's Encrypt certificate monthly https://letsencrypt.readthedocs.org/en/latest/using.html#renewal

[Timer]
OnCalendar=monthly
Persistent=true

[Install]
WantedBy=multi-user.target

Enable and start the timer, and you should be all set!

systemctl enable letsencrypt.timer
systemctl start letsencrypt.timer

systemctl list-timers

Since this project is in beta and is still in very rapid development I expect this whole thing to break every now and then, I will update it as I need to fix things with the relevant changes!

Update

Here is an example nginx configuration file for tighter security, but less compatibility, thanks to this post on the Let’s Encrypt community and examples from this github repo.

Create / edit the file /etc/letsencrypt/cli.ini and add the following:

rsa-key-size = 4096

Generate the DH key and ticket:

sudo mkdir -p /etc/nginx/ssl &&
sudo openssl rand 48 -out /etc/nginx/ssl/ticket.key &&
sudo openssl dhparam -out /etc/nginx/ssl/dhparam4.pem 4096

Modify your nginx config to look similar to the following:

server {
        listen          443 ssl http2;
        listen          [::]:443 ssl http2;
        server_name     .example.com;

        add_header Strict-Transport-Security "max-age=63072000";
        add_header X-Frame-Options DENY;

        ssl on;
        ssl_certificate         /etc/letsencrypt/yourcerts/fullchain.pem;
        ssl_certificate_key     /etc/letsencrypt/yourcerts/privkey.pem;

        ssl_stapling on;
        ssl_stapling_verify on;
        ssl_trusted_certificate /etc/letsencrypt/yourcerts/fullchain.pem;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;

        ssl_ecdh_curve secp384r1;

        ssl_session_cache shared:SSL:100m;
        ssl_session_timeout 24h;
        ssl_session_tickets on;
        ssl_session_ticket_key /etc/nginx/ssl/ticket.key;

        ssl_dhparam /etc/nginx/ssl/dhparam4.pem;

        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:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';

        resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 216.146.35.35 216.146.36.36 valid=300s;
        resolver_timeout 3s;

        error_log   /path/to/logs;
        access_log  off;

        gzip on;
        gzip_disable msie6;
        gzip_static on;
        gzip_comp_level 9;
        gzip_proxied any;
        gzip_min_length  1100;
        gzip_buffers 16 8k;
        gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;

        root /path/to/vhost/html;
        index index.php index.html;

        location / {
                autoindex on;
                try_files $uri $uri/ @wordpress @extensionless-php;
        }

        # Cache Static Files
        location ~* \.(html|css|js|png|jpg|jpeg|gif|ico|svg|eot|woff|ttf)$ { expires max; }
        
        # Do not allow access to hidden files.
        location ~ /\. { deny all; }
       
        # Enable extensionless PHP
        location @extensionless-php {
                rewrite ^(.*)$ $1.php last;
        }

        location ~ \.php$ {
                try_files $uri @wordpress;
                include       /etc/nginx/fastcgi.conf;
                fastcgi_index index.php;
                fastcgi_pass  unix:/var/run/php-fpm/php-fpm.sock;
        }

        location @wordpress {
                include       /etc/nginx/fastcgi.conf;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root/index.php;
                fastcgi_param SCRIPT_NAME     /index.php;
                fastcgi_pass  unix:/var/run/php-fpm/php-fpm.sock;
        }


        location ~ /\.ht {
                deny all;
        }

        error_page 404 = @wordpress;
}

 

One thought on “SSL in Nginx using Let’s Encrypt

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.