TLS Certificates on Linux: Let's Encrypt, certbot, and What Actually Happens

TLS Certificates on Linux: Let's Encrypt, certbot, and What Actually Happens

Encrypted connections used to cost money and paperwork. A certificate meant paying a commercial authority, proving your identity to someone’s satisfaction, and manually installing a file that expired a year later, usually on a weekend. Let’s Encrypt made certificates free, automated, and short-lived, and the whole industry moved.

What a certificate does

A TLS certificate binds a public key to a domain name, signed by an authority your browser already trusts. When a client connects, the server presents the certificate, the client checks the signature chain against its trust store, and the two negotiate an encrypted session.

That gets you two things: nobody between you and the server can read or modify the traffic, and you know you reached the domain you typed rather than something impersonating it.

It does not tell you the site is run by honest people. A domain validated certificate proves control of a domain and nothing else, which is worth remembering whenever someone points at a padlock as evidence of legitimacy.

The ACME challenge

Let’s Encrypt automates the part that used to involve a human. The ACME protocol asks you to prove control of the domain by doing something only the controller could do.

HTTP-01 is the common one. The CA gives your client a token; your client writes it to /.well-known/acme-challenge/<token> on your web server; the CA fetches it over plain HTTP on port 80. If it matches, you control the domain.

DNS-01 asks you to publish a TXT record at _acme-challenge.yourdomain.com. It requires no inbound connectivity at all, which makes it the answer for servers behind NAT, and it is the only way to obtain a wildcard.

TLS-ALPN-01 validates over port 443 using a special ALPN protocol, useful when port 80 is unavailable.

Getting a certificate

sudo apt install certbot python3-certbot-nginx    # Debian and Ubuntu
sudo dnf install certbot python3-certbot-nginx    # Fedora

With nginx already serving the domain, the plugin does everything including editing your config:

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

It obtains the certificate, adds the TLS directives, sets up the HTTP to HTTPS redirect, and installs a renewal timer.

If you would rather it not touch your configuration:

sudo certbot certonly --nginx -d example.com

Certificates land in /etc/letsencrypt/live/example.com/. Point your server at fullchain.pem and privkey.pem. The reverse proxy config builder generates a server block already referencing those paths.

Standalone, when nothing is listening yet

sudo certbot certonly --standalone -d example.com

certbot runs its own temporary web server on port 80, which means stopping whatever is using it first.

DNS-01 and wildcards

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  -d example.com -d '*.example.com'

Plugins exist for most large DNS providers. The credentials file holds an API token and must be chmod 600, because it can rewrite your DNS.

Wildcards look tempting when you host many subdomains. The tradeoff is that one private key then covers everything, so a compromise is total rather than partial.

Renewal, and testing that it works

Certificates last 90 days and certbot attempts renewal at 30 days remaining, giving a month of margin before anything breaks.

Installation sets up a systemd timer automatically:

systemctl list-timers certbot.timer

The important step is testing it, which almost nobody does:

sudo certbot renew --dry-run

This runs the entire renewal process against the staging environment without replacing your live certificate. If it passes, renewal works. If it fails, you have found out with weeks to spare rather than through a browser warning.

The usual failure is a configuration change made months earlier that broke the challenge path, often a location block that now returns 404 for /.well-known/.

Reloading after renewal

A renewed certificate does nothing until the service reloads it.

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

Or place a script in /etc/letsencrypt/renewal-hooks/deploy/, which runs after any successful renewal.

Rate limits

Let’s Encrypt enforces limits that catch people during setup: a cap on certificates per registered domain per week, and a tighter cap on duplicate certificates for the same name set.

Use the staging environment while experimenting:

sudo certbot certonly --staging --nginx -d example.com

Staging issues untrusted certificates, so browsers complain, but the process is identical and the limits are far higher. Switch to production once the mechanics work.

Caddy, if you would rather not think about it

Caddy obtains and renews certificates automatically with no additional tooling. A minimal config is two lines:

example.com {
    reverse_proxy 127.0.0.1:8080
}

Ports 80 and 443 open, domain resolving to the machine, and it handles the rest. For a homelab this is a legitimate reason to prefer it over nginx.

Internal services

Public CAs will not issue certificates for names that do not resolve publicly, which is a problem for services on a LAN.

The options are: use DNS-01 with a real domain and point the records at private addresses, which works and is what most people do; run your own internal CA and distribute the root certificate to your devices; or reach services over a VPN and use real certificates on real names. Our WireGuard explainer covers the last approach.

Self-signed certificates work but train users to click through warnings, which is a habit worth not building.

Checking what you have

# Expiry and subject of a live site
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates -subject

# What certbot is managing
sudo certbot certificates

# Verify the chain is complete, a common misconfiguration
openssl s_client -connect example.com:443 -showcerts </dev/null

An incomplete chain is the classic subtle failure: it works in your browser, which caches intermediates, and fails for API clients and mobile apps that do not. Always serve fullchain.pem, never cert.pem alone.

Frequently Asked Questions

How does Let’s Encrypt verify I own a domain?

Through an ACME challenge. The HTTP-01 challenge asks you to serve a specific file at a well-known path on port 80. The DNS-01 challenge asks you to publish a specific TXT record. Passing either proves control of the domain, which is all a domain validated certificate attests to.

Why do Let’s Encrypt certificates expire after 90 days?

Short lifetimes limit the damage from a compromised key and force automation, which is the actual goal. A certificate you have to remember to renew annually will eventually be forgotten. One that renews itself every 60 days either works continuously or fails loudly and early.

Do I need to open port 80 if my site is HTTPS only?

For the HTTP-01 challenge, yes, because the validation server connects over plain HTTP. You can redirect everything else to HTTPS and still serve the challenge path. Alternatively use the DNS-01 challenge, which needs no inbound ports at all.

How do I get a wildcard certificate?

Only through the DNS-01 challenge, which means your DNS provider needs an API and a certbot plugin for it. Wildcards are convenient for many subdomains but they also mean one key protects everything, so separate certificates are often the better choice.

Is my certificate renewing automatically?

Test it rather than assuming. Run certbot renew --dry-run, which performs the full renewal against the staging environment without touching your real certificate. Also confirm the systemd timer is active with systemctl list-timers certbot.timer.

What does a certificate actually prove?

For a domain validated certificate, only that whoever obtained it controlled the domain at the time. It does not vouch for the operator, the content, or the trustworthiness of the site. The padlock means the connection is encrypted and you reached the domain in the address bar, nothing more.