Free SSL with Let's Encrypt and Certbot on nginx
I used to dread certificate renewals: the CSR, the email dance, pasting files into place, then forgetting until the browser warning showed up. Let's Encrypt is free and Certbot does the issuing and renewing. The 90-day lifetime sounds annoying until you realise it forces you to automate the whole thing.
Here's my setup on Ubuntu 16.04 with nginx, using the webroot method. I prefer it to the nginx plugin because Certbot never edits my nginx config. CentOS 7 notes are included where packages differ. You need nginx already answering on port 80 from the Internet, and DNS A records for example.com and www.example.com pointing at the server, since that's how Let's Encrypt validates.
Install Certbot
The Ubuntu 16.04 package is old. Use the Certbot PPA:
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot
certbot --version
On CentOS 7, certbot is in EPEL: yum install epel-release && yum install certbot.
A challenge location in nginx
Let's Encrypt checks domain control by fetching http://example.com/.well-known/acme-challenge/<token>. I don't let Certbot write into the site's document root; I point that path at a dedicated directory. In the HTTP server block:
server {
listen 80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}
Leave out the return 301 until the certificate exists. Create the directory and reload:
sudo mkdir -p /var/www/letsencrypt
sudo nginx -t && sudo systemctl reload nginx
Get the certificate
sudo certbot certonly --webroot -w /var/www/letsencrypt \
-d example.com -d www.example.com \
--email admin@example.com --agree-tos --no-eff-email
On success Certbot prints the path of the new files:
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem
These are symlinks into /etc/letsencrypt/archive/ that get updated on renewal, so the nginx config never has to change.
The HTTPS server block
Add a second block for port 443:
server {
listen 443 ssl;
server_name example.com www.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 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=15768000" always;
root /var/www/example.com/html;
index index.html;
}
Now enable the return 301 redirect in the port 80 block, open port 443 in the firewall (ufw allow 443/tcp or firewall-cmd --permanent --add-service=https), and reload nginx.
Renewal
The PPA package installs a systemd timer (certbot.timer) that runs certbot renew twice a day. Certbot only renews within 30 days of expiry, so that's fine. What the timer doesn't do is reload nginx, and I learned that by staring at an expired cert on a server that had "renewed" fine. Add a deploy hook:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
On CentOS 7 there is no timer by default; add a cron entry in /etc/cron.d/certbot:
0 3,15 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx"
Check it
A dry run exercises the whole challenge path against the staging server without issuing anything:
sudo certbot renew --dry-run
Then check what the browser will see:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -issuer -dates
The issuer should be Let's Encrypt Authority X3 and notAfter about 90 days out. systemctl list-timers | grep certbot confirms the timer is scheduled.
Errors I've seen
Failed authorization procedure ... Invalid response from http://example.com/.well-known/acme-challenge/...: 404: nginx isn't serving the challenge directory. Put a file at/var/www/letsencrypt/.well-known/acme-challenge/testand fetch it with curl. Anotherlocationblock may be matching first;^~avoids that.Connection refusedduring validation: port 80 is closed at the firewall or DNS points elsewhere.too many certificates already issued for exact set of domains: rate limit, 5 per week per exact domain set. Use--stagingwhile experimenting.- Renewed but browsers show the old cert: the deploy hook is missing or not executable. Run
certbot renew --force-renewalonce to test it, but don't make a habit of it.
I still keep an expiry check in a monitoring script. Automation fails quietly and 90 days is short.