KBeezie

There's no place like ::1

Menu
  • Home
  • Start Here
  • Security Series
  • About

Let's Encrypt Without Certbot Touching Your Nginx Config

2026/05/11 in Security

Certbot's Nginx plugin is convenient — it finds your server blocks, edits them, and wires up SSL in one command. It's also a liability if you maintain your Nginx configuration by hand. The plugin can reorder directives, strip comments, reformat your carefully indented blocks, and sometimes leave behind config debris when you remove a domain. The alternative is simpler than it sounds: tell Certbot to fetch the certificate and do nothing else. You keep full control of your Nginx configuration.

Install Certbot

Certbot recommends their official installation instructions for your specific OS, but the short version for most modern systems:

# Debian/Ubuntu
sudo apt install certbot

# RHEL/Rocky/Alma (EPEL required)
sudo dnf install certbot

You don't need python3-certbot-nginx or certbot-nginx — those are the Nginx plugin packages. You only need the base certbot package, which ships with the certonly subcommand.


The shared webroot — one path for all domains

Certbot validates domain ownership by placing a temporary file in your webroot and having the Let's Encrypt servers fetch it over HTTP. The default approach ties the webroot to each domain's document root, but there's a cleaner way: use a single directory shared across all your server blocks.

Create a webroot that Nginx can serve regardless of which domain is being validated:

sudo mkdir -p /usr/share/nginx/html/.well-known/acme-challenge
sudo chown -R www-data:www-data /usr/share/nginx/html

Then in every server block — both HTTP and HTTPS — or in a catch-all server block that answers on port 80, add:

location ~ /.well-known/acme-challenge {
    root /usr/share/nginx/html;
    allow all;
}

If you use a catch-all HTTP block that redirects everything to HTTPS, the location ~ takes priority over the redirect (regex locations are processed before prefix locations), so the validation request is served directly without a redirect loop. If you'd rather not rely on that ordering, define it as location ^~ or place it in a dedicated port 80 server block that doesn't redirect.

Now any domain pointed at your server can complete HTTP validation through the same directory — no per-site configuration needed.


Issue a certificate

Use certonly (not run) to fetch a certificate without Certbot touching your Nginx config:

sudo certbot certonly --webroot \
  --webroot-path /usr/share/nginx/html \
  -d mysite.com \
  -d www.mysite.com \
  --cert-name mysite.com
  • certonly — Fetch the certificate only. Don't install or modify any server configuration.
  • --webroot — Use HTTP validation by placing a temporary file in your webroot for Let's Encrypt to fetch.
  • --webroot-path — The directory Nginx serves .well-known/acme-challenge from. This is the shared path you set up earlier.
  • -d — A domain to include in the certificate. Repeat for each subdomain or additional domain you want covered.
  • --cert-name — A human-readable name for this certificate. Defaults to the first -d domain if omitted, but being explicit avoids Certbot appending -0001 if you ever reissue.

Your certificate lands in /etc/letsencrypt/live/mysite.com/ — the name you gave it, not an auto-generated label:

ls /etc/letsencrypt/live/
# mysite.com/  anothersite.com/  

Naming certificates by domain makes them easy to identify at a glance. You can list them, check expiry dates, or remove old ones without decoding cryptic directory names:

sudo certbot certificates

Wire it into Nginx

Now you reference the certificate paths manually in your server block — no Certbot-managed includes, no auto-generated symlink names:

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

    ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/mysite.com/chain.pem;

    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # ... rest of your configuration ...
}

The options-ssl-nginx.conf and ssl-dhparams.pem files are generated the first time you run Certbot (even with certonly). They contain Mozilla-recommended TLS settings and a 4096-bit DH parameters file — safe to share across all your server blocks.


Set up automatic renewal

Certbot certificates are valid for 90 days. A cron job handles renewal before expiry:

# /etc/cron.d/certbot — or add to root's crontab
45 6 * * * root certbot renew \
  --webroot --webroot-path /usr/share/nginx/html \
  --quiet \
  --renew-hook "systemctl reload nginx"

A few things about this crontab entry:

  • 45 6 — Runs at 6:45 AM daily. Certbot only actually attempts renewal when certificates are within 30 days of expiry; the rest of the time it's a no-op that completes in under a second.
  • --webroot — Tells renew to use the webroot authenticator for all certificates. Certbot remembers which plugin issued each cert; this ensures it uses the right one.
  • --webroot-path — The same shared directory. This applies to every certificate being renewed, so it only works if all your certs were issued with the same webroot path.
  • --quiet — Suppresses output on success. Cron will still email you on failure if your system is configured for it.
  • --renew-hook — Runs only if at least one certificate was actually renewed. A simple Nginx reload picks up the new files without dropping connections.

If you have certificates issued through different methods (some webroot, some DNS), you can still use certbot renew without the --webroot flag — Certbot will use whatever authenticator each certificate was originally issued with.


Adding domains to an existing certificate

You added a new subdomain and want it covered by the same certificate:

sudo certbot certonly --webroot \
  --webroot-path /usr/share/nginx/html \
  --cert-name mysite.com \
  -d mysite.com -d www.mysite.com -d blog.mysite.com

You must list all domains you want on the certificate, including the original ones — Certbot replaces the certificate, it doesn't append. If you forget an existing domain, it's dropped. After reissue, reload Nginx.


Removing a certificate

When you retire a site, clean up its certificate:

sudo certbot delete --cert-name mysite.com

This removes the certificate and its renewal configuration. Your Nginx server block still references the old paths — Certbot won't clean those up for you, and you'll want to remove or archive the server block yourself.


Checking certificate status

# List all certificates and their expiry dates
sudo certbot certificates

# Dry-run renewal to check for problems
sudo certbot renew --dry-run

Why this approach?

Your Nginx configuration is yours. Certbot never touches it. You can use includes, split configurations across files, version them in git, and deploy them with Ansible — Certbot's plugin doesn't know about any of that. When you remove a domain, you delete its server block and its certificate independently. When you add one, you write the configuration once and issue the certificate once. No round-trips through Certbot's auto-generated edits, no leftover server { } blocks from certificates you removed, no comments silently stripped from your hand-tuned config.

For a deeper dive into the webroot authenticator: Certbot Webroot Documentation

Technical Audit Summary

This guide is maintained as part of a modular, SSL-first framework. Each configuration is audited for production stability and modern security standards.

Last Audit: May 2026
Environment: Debian Trixie (13)
Nginx: 1.30.0
PHP-FPM: 8.5.5

Compatibility: Tested against current stable releases. While optimized for the stack above, core logic remains relevant for Nginx 1.26+ and PHP 8.2+ environments.

Tags: letsencrypt, certbot, ssl, nginx
©2026 KBeezie | Disclaimer | Privacy Notice