How to Secure Nginx with Let’s Encrypt and Certbot

How to Secure Nginx with Let's Encrypt and Certbot

There’s no good excuse left for running a public website without HTTPS. Let’s Encrypt made free, automated TLS certificates the norm, and Certbot remains the most widely used tool for obtaining and renewing them on Nginx servers. In this guide, I’ll walk through the full process — from installing Certbot to getting auto-renewal working reliably — along with the security and performance tuning you should layer on top of a basic certificate.

What Let’s Encrypt and Certbot Actually Do

Let’s Encrypt is a free, automated certificate authority. It issues domain-validated TLS certificates that are trusted by essentially every modern browser and operating system. Certificates are valid for 90 days, which is intentionally short to encourage automation rather than manual renewal.

Certbot is the official client most people use to interact with Let’s Encrypt. It can automatically:

  • Prove domain ownership via the ACME protocol (usually through an HTTP challenge or DNS challenge).
  • Request and download the certificate.
  • Optionally edit your Nginx configuration directly to enable HTTPS.
  • Set up automatic renewal so you never have to think about expiration again.

Requirements

  • A registered domain name pointing to your server’s public IP address (an A or AAAA DNS record). Let’s Encrypt validates domain ownership over the internet, so this has to be resolvable publicly — it won’t work for purely internal/private domains without a DNS-01 challenge.
  • Nginx installed and serving at least a basic site on port 80.
  • Ports 80 and 443 open in your firewall and any cloud security groups.
  • Root or sudo access on the server.

Check your firewall if you’re using ufw:

sudo ufw allow 'Nginx Full'
sudo ufw status

Installing Certbot

The recommended installation method today is via snap, since it keeps Certbot up to date independent of your distribution’s package repositories, which often lag behind.

sudo apt update
sudo apt install snapd
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

On distributions where snap isn’t your preference, most package managers also carry Certbot directly:

# Debian/Ubuntu
sudo apt install certbot python3-certbot-nginx

# RHEL/CentOS/Rocky/Alma
sudo dnf install certbot python3-certbot-nginx

The python3-certbot-nginx package is important — it’s the Nginx plugin that lets Certbot automatically edit your server blocks and reload Nginx for you.

Verify the install:

certbot --version

Preparing Your Nginx Configuration

Before running Certbot, make sure you have a working HTTP server block with the correct server_name:

server {
    listen 80;
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Confirm the site is reachable over plain HTTP first — Certbot’s HTTP-01 challenge needs to reach http://example.com/.well-known/acme-challenge/... successfully before it can issue a certificate.

Obtaining a Certificate with the Nginx Plugin

The simplest path is letting Certbot handle everything, including editing your Nginx config:

sudo certbot --nginx -d example.com -d www.example.com

Certbot will:

  1. Verify domain ownership using a temporary file served through your existing HTTP block.
  2. Download the certificate and private key to /etc/letsencrypt/live/example.com/.
  3. Ask whether you want to redirect all HTTP traffic to HTTPS (say yes, unless you have a specific reason not to).
  4. Automatically modify your Nginx server block to add the listen 443 ssl; directive and certificate paths.

After it finishes, your config will look something like this:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.html;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        try_files $uri $uri/ =404;
    }
}

Obtaining a Certificate Without Auto-Editing Nginx

If you’d rather manage your Nginx config manually (which I usually prefer for anything beyond a simple site, so I can control exactly what’s added), use the certonly mode with the webroot method:

sudo certbot certonly --webroot -w /var/www/example.com \
  -d example.com -d www.example.com \
  --email you@example.com --agree-tos --no-eff-email

This drops the certificate files in the same location without touching your Nginx configuration, letting you wire up the ssl_certificate directives yourself.

Getting a Wildcard Certificate (DNS-01 Challenge)

If you need a wildcard certificate (*.example.com), you must use the DNS-01 challenge instead of HTTP-01, since Let’s Encrypt needs proof of control over the domain via a DNS TXT record rather than an HTTP file.

sudo certbot certonly --manual --preferred-challenges dns \
  -d example.com -d '*.example.com'

Certbot will pause and ask you to create a specific TXT record at your DNS provider before continuing. For full automation, use a DNS plugin matching your provider instead of the manual method — for example, Cloudflare:

sudo apt install python3-certbot-dns-cloudflare
sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d example.com -d '*.example.com'

This lets renewal happen fully unattended, since the plugin can programmatically create and remove the DNS TXT record via the Cloudflare API.

Complete Example Configuration

Here’s a hardened, production-style HTTPS server block:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.html;

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

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        try_files $uri $uri/ =404;
    }
}

I’m deliberately restricting ssl_protocols to TLSv1.2 and TLSv1.3 — older protocols like TLSv1.0/1.1 and SSLv3 have known vulnerabilities and should not be enabled on a modern server.

Setting Up Automatic Renewal

Since Let’s Encrypt certificates expire every 90 days, renewal has to be automated or your site will eventually start throwing certificate errors. Certbot installs a systemd timer or cron job automatically during installation, but it’s worth verifying it exists:

systemctl list-timers | grep certbot

Or, if using cron:

sudo cat /etc/cron.d/certbot

Test the renewal process without actually renewing (a dry run):

sudo certbot renew --dry-run

If you edited your Nginx config manually and used certonly, you’ll want a post-renewal hook to reload Nginx after each successful renewal:

sudo certbot renew --dry-run --deploy-hook "systemctl reload nginx"

To make this permanent, create a deploy hook script:

sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/bin/bash
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Certbot automatically runs every script in that directory after a successful renewal — no cron editing required.

Testing Your Configuration

Validate Nginx syntax:

sudo nginx -t
sudo systemctl reload nginx

Check your certificate is live and correctly served:

curl -I https://example.com

Use SSL Labs for a comprehensive external check of your TLS configuration, cipher strength, and certificate chain:

https://www.ssllabs.com/ssltest/analyze.html?d=example.com

Aim for an A or A+ rating — it’ll flag weak ciphers, missing HSTS, or protocol issues you might have missed.

Check certificate expiration directly from the command line:

echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -dates

Troubleshooting Common Issues

“Challenge failed” during certificate issuance. Almost always a DNS or firewall problem — confirm your domain actually resolves to the server’s IP (dig example.com), and that port 80 is open and reachable from the public internet, not just locally.

Certbot succeeded but the site still shows “not secure.” Double-check your Nginx server block is actually listening on 443 with the correct certificate paths, and that you reloaded Nginx after any manual changes. Also confirm you’re not being served from a cached HTTP version via a browser extension or old bookmark.

Renewal fails silently and the cert eventually expires. Set up a monitoring check (many uptime monitors support certificate expiration alerts) so you get notified before it becomes a live outage rather than discovering it when a user reports a browser warning.

Certificate works for example.com but not www.example.com. Make sure both were included with -d flags when the certificate was issued, and that your server_name directive lists both.

Mixed content warnings after enabling HTTPS. This means some resources on the page (images, scripts, stylesheets) are still being loaded via hardcoded http:// URLs. Update those references to https:// or protocol-relative URLs.

Security Considerations

  • Enable HSTS (Strict-Transport-Security) once you’re confident HTTPS is fully working, but be cautious with preload — submitting your domain to the HSTS preload list is essentially permanent and affects every subdomain, so test thoroughly first.
  • Disable outdated TLS versions and weak ciphers, as shown in the example config above.
  • Keep private keys (privkey.pem) readable only by root and Nginx’s worker user — never expose the /etc/letsencrypt/live/ directory publicly.
  • Enable OCSP stapling (ssl_stapling on;) so browsers don’t need to make a separate request to the certificate authority to check revocation status, which also improves handshake performance slightly.
  • Rotate and audit any API tokens used for DNS-01 automation (like your Cloudflare API token) with the minimum required scope.

Performance Tips

  • Enable HTTP/2 (listen 443 ssl http2;) — it multiplexes requests over a single connection and meaningfully improves page load times for sites with many assets.
  • Use ssl_session_cache and ssl_session_tickets to reduce the cost of repeat TLS handshakes for returning visitors.
  • OCSP stapling avoids an extra round-trip for certificate validation, shaving measurable time off the initial handshake.
  • Consider a shorter ssl_session_timeout if you’re concerned about session ticket key rotation and forward secrecy trade-offs versus raw performance.

Real-World Use Cases

  • Personal blogs and portfolio sites: quick, free HTTPS with zero ongoing cost.
  • Small business websites: professional trust indicators (the padlock) matter for customer confidence and are effectively required for e-commerce.
  • Internal tools and dashboards: even on private networks, HTTPS prevents credential sniffing on shared infrastructure.
  • Multi-domain hosting: a single server issuing and renewing certificates for dozens of virtual hosts automatically.
  • API backends: encrypting traffic between clients and your API is now a baseline expectation, and many client libraries and mobile app stores actively reject plain HTTP endpoints.

Best Practices

  • Automate renewal from day one — don’t rely on remembering to renew manually.
  • Always redirect HTTP to HTTPS so users (and search engines) consistently land on the secure version.
  • Test your SSL configuration with SSL Labs after any change to ssl_protocols or ssl_ciphers.
  • Keep Certbot itself updated, especially if installed via a package manager rather than snap.
  • Use a deploy hook to reload Nginx after renewal if you’re managing configuration manually.
  • Monitor certificate expiration independently of Certbot’s own renewal job, as a safety net in case automation silently breaks.

Once this is set up, HTTPS essentially disappears as a concern — certificates renew themselves quietly in the background, and you get to stop thinking about it until you add a new domain or subdomain to the mix.

Total
1
Shares

Leave a Reply

Previous Post
How to Configure Nginx as a WebSocket Proxy

How to Configure Nginx as a WebSocket Proxy

Next Post
How to Set Up Nginx with Docker

How to Set Up Nginx with Docker

Related Posts