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”