
How to Secure Nginx with Let's Encrypt
Getting a trusted TLS certificate on a public Nginx server takes less than five minutes with Certbot and Let's Encrypt. In this guide I'll walk through the full setup: installing Certbot, obtaining a certificate, verifying the HTTPS configuration, and confirming that automatic renewal is in place. I'll also cover what Certbot actually does to your Nginx config so nothing is a black box.
This guide assumes Ubuntu 20.04 or later and a running Nginx server with a domain already pointing to it.
Prerequisites
- A running Nginx server on Ubuntu with
sudoaccess - A domain with DNS
Arecords pointing to your server's public IP, for exampledevops-by-example.ioandwww.devops-by-example.io - Port 80 and 443 open in your firewall (both are required by Certbot during the HTTP-01 challenge)
- A basic Nginx server block configured for your domain (see below)
Goals
- Obtain a free 90-day TLS certificate from Let's Encrypt using Certbot
- Understand how the HTTP-01 domain validation challenge works
- Confirm that Certbot has configured Nginx correctly and set up automatic renewal
- Know what to check when renewal fails
Nginx Server Block Before Certbot
Before running Certbot, your Nginx config should have a server block that references your domain name. Here is a minimal starting point:
server {
listen 80;
server_name devops-by-example.io www.devops-by-example.io;
root /var/www/devops-by-example;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}Verify the config is valid and that Nginx is running before proceeding:
sudo nginx -t
sudo systemctl status nginxAlways confirm syntax is ok and test is successful before making changes. Certbot will modify this file, and starting from a broken config makes debugging harder.
Install Certbot
Install Certbot and its Nginx plugin from the Ubuntu package repository:
sudo apt install certbot python3-certbot-nginxThe python3-certbot-nginx plugin is what allows Certbot to read and modify Nginx config files directly. Without it, Certbot can still obtain a certificate but it cannot configure Nginx automatically.
How Let's Encrypt Validates Your Domain
Before issuing a certificate, Let's Encrypt needs proof that you control the domain. Certbot supports several challenge types. This guide uses the HTTP-01 challenge, which is the default when you use the --nginx flag.
The HTTP-01 challenge works as follows:
- Certbot creates a temporary file under
/.well-known/acme-challenge/on your web server. - Let's Encrypt's servers make an HTTP request to
/.well-known/acme-challenge/<token. - If the request returns the correct token, domain ownership is confirmed and the certificate is issued.
This is why port 80 must be open. Let's Encrypt needs to reach your server over HTTP to complete the challenge. If your server is behind a load balancer or firewall that blocks port 80, you would need to use the DNS-01 challenge instead.
For wildcard certificates (
*.example.com) the HTTP-01 challenge is not supported. Those require DNS-01, which involves creating a TXT record in your DNS. My post on wildcard SSL certificates covers that setup.
Run Certbot
sudo certbot --nginx -d devops-by-example.io -d www.devops-by-example.ioCertbot will walk through a short interactive process:
- Email address: use a real address. Let's Encrypt sends expiry warnings to this email if renewal fails and the certificate is approaching the 90-day limit.
- Terms of Service: agree to proceed.
- Redirect HTTP to HTTPS: choose option 2 to redirect all HTTP traffic to HTTPS. Certbot adds the redirect rules to your Nginx config automatically.
If the challenge succeeds, Certbot outputs the certificate paths:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/devops-by-example.io/fullchain.pem
Key is saved at: /etc/letsencrypt/live/devops-by-example.io/privkey.pemWhat Certbot Does to Your Nginx Config
Certbot modifies the Nginx server block in place. After a successful run, your config will look something like this:
server {
server_name devops-by-example.io www.devops-by-example.io;
root /var/www/devops-by-example;
index index.html;
location / {
try_files $uri $uri/ =404;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/devops-by-example.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/devops-by-example.io/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
if ($host = devops-by-example.io) {
return 301 https://$host$request_uri;
}
listen 80;
server_name devops-by-example.io www.devops-by-example.io;
return 404;
}The original server block gets listen 443 ssl added along with the certificate paths. A new HTTP block handles the redirect from port 80 to HTTPS. The /etc/letsencrypt/options-ssl-nginx.conf file sets recommended TLS protocol and cipher settings. You should review this file once but you generally do not need to edit it.
Verify the Certificate
Open an incognito window and navigate to https://devops-by-example.io. Click the padlock icon and inspect the certificate. It should:
- Show "Let's Encrypt" as the issuer
- Cover both
devops-by-example.ioandwww.devops-by-example.io - Be valid for up to 90 days from the issue date
You can also inspect it from the command line:
openssl s_client -connect devops-by-example.io:443 -servername devops-by-example.io < /dev/null 2>/dev/null | openssl x509 -text -noout | grep -A2 "Subject:"To check the full certificate chain and see the intermediate CA:
openssl s_client -connect devops-by-example.io:443 -showcerts < /dev/null 2>/dev/nullAutomatic Renewal
Let's Encrypt certificates are valid for 90 days. Certbot renews them automatically at the 60-day mark, well before expiry.
Certbot installs a systemd timer (preferred on Ubuntu 20.04+) or a cron job depending on your system:
# Check the systemd timer
systemctl status certbot.timer
# Or check the cron job
cat /etc/cron.d/certbotThe cron entry typically runs twice a day:
0 */12 * * * root test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot renew -qThe sleep int(rand(43200)) adds a random delay of up to 12 hours before attempting renewal. This is intentional. Let's Encrypt asks clients to spread renewal attempts over time rather than all running at the same moment.
To verify renewal would succeed without actually renewing:
sudo certbot renew --dry-runNo errors means the renewal path is clean. Run this after any changes to your Nginx config or firewall rules. It is also worth running after OS upgrades or if you change the domain DNS setup.
What to Check When Renewal Fails
If renewal fails, Let's Encrypt sends a warning email around day 80 when the certificate is 10 days from expiry. Here is how to diagnose the common causes:
Port 80 blocked. The HTTP-01 challenge requires port 80. If a firewall change closed it, renewal will fail. Verify with:
curl -v http://devops-by-example.io/.well-known/acme-challenge/testIf this returns a connection error, check your firewall or security group rules.
Nginx serving the wrong root. Certbot places challenge files under the document root configured in your Nginx server block. If the root has changed or the directory does not exist, the challenge file will return a 404.
Rate limits. Let's Encrypt enforces rate limits: 50 certificates per registered domain per week. If you are running certbot in a loop during testing, you may hit this limit. Use --test-cert flag to issue from the staging environment during development. Staging certificates are not trusted by browsers but the issuance process is identical.
Permissions. Certbot runs as root and writes certificates to /etc/letsencrypt/live/. If the Nginx worker process cannot read these files, reload Nginx after any file permission changes:
sudo nginx -t && sudo systemctl reload nginxConclusion
With Certbot and the Nginx plugin, obtaining a trusted TLS certificate is a single command. The important things to keep in mind going forward:
- Certbot sets up automatic renewal, but you should verify it with
--dry-runafter any infrastructure changes - Port 80 must stay accessible for HTTP-01 renewal to work. If you close it, switch to DNS-01 or set up a firewall exception for Let's Encrypt's IP ranges
- Let's Encrypt staging (
--test-cert) is useful when testing Certbot automation so you do not burn through rate limits - For wildcard certificates covering all subdomains, the HTTP-01 challenge does not apply. You need the DNS-01 challenge with a DNS provider that supports API access
Comments