
A full-screen browser warning where a homepage should be, and two phone calls asking whether the site has been hacked. It has not. A certificate expired, quietly, ninety days after somebody installed it by hand while following a tutorial that never mentioned expiry at all. Nothing is misconfigured, no disk is full, no attacker is involved: a clock ran out while nobody was watching it. Getting the site back takes about ten minutes. Making sure the same thing cannot happen again takes another twenty, and that second part is the one worth your attention, because the quick repair only resets the same ninety-day timer. Certificates have been free and automatic for a decade, which changes what an expired one actually means. It is almost never a story about money that wasn’t spent. It is a story about automation that was started and never finished, and this article is about finishing it.
what a certificate actually proves

A TLS certificate does two jobs and people usually only think about one of them. The first is encryption: traffic between the browser and your server is unreadable to anyone sitting in between, which on an Indian public Wi-Fi network is not a theoretical concern. The second job is identity, and it is narrower than most people assume. A Let’s Encrypt certificate proves that whoever requested it controlled the DNS or the web root for that domain name at the moment of issuance. That’s it.
the ACME dance, in plain terms


ACME is the protocol that removed humans from certificate issuance. Your client generates a key pair, asks the certificate authority for a certificate covering some names, and the authority replies with a challenge: prove you control these names. Your client satisfies the challenge, the authority checks it, and a certificate comes back. The whole exchange takes a few seconds and no email verification link is involved anywhere.
HTTP-01: the default, and it’s fine
With HTTP-01, your client writes a token file under /.well-known/acme-challenge/ on your web root, and the authority fetches it over plain HTTP on port 80 from the public internet. If it reads back the expected value, you’ve proved control. This is what runs on most sites and it needs nothing clever from you beyond a reachable port 80 and DNS that already points at the box.
DNS-01: wildcards and servers behind CGNAT
DNS-01 proves control by asking you to publish a TXT record at _acme-challenge.yourdomain. The authority resolves that record instead of fetching a file, which means your server never has to be reachable from outside at all. Two situations make this the only sensible option.
Wildcards are the first. A certificate for *.technobabu.com can only be issued through DNS-01, full stop. If you run a handful of subdomains for internal services, one wildcard beats managing eight separate certificates. The second is a machine on a home connection behind carrier-grade NAT, which is common on Indian home broadband unless you’ve specifically asked your ISP for a public address. There is no inbound port 80 to hit, so file-based validation cannot work, but a TXT record you publish yourself works perfectly.
| Concern | HTTP-01 | DNS-01 |
|---|---|---|
| What it checks | A file on your web root | A TXT record in your zone |
| Needs inbound port 80 | Yes, for issue and renewal | No |
| Wildcard certificates | Not possible | The only way |
| Works behind CGNAT | No | Yes |
| Setup effort | Almost none | A DNS API token, scoped to the one zone if your DNS host allows it, stored in a root-only file (chmod 600) |
| Main failure cause | Firewall, or a rewrite rule that serves something other than the token | Propagation delay, wrong zone |
issuing your first certificate with certbot


I’ll use certbot with the nginx plugin, because it’s the most common pairing. Get nginx serving your domain over plain HTTP first; my nginx setup guide covers the server block, and certbot behaves far better when there’s a valid server_name for it to find.
sudo apt update
sudo apt install certbot python3-certbot-nginx
# confirm DNS resolves to this box before you ask for anything
dig +short technobabu.com
curl -s ifconfig.me
Those last two lines are the thirty-second check that saves you from the most common failure: an A record still pointing at an old shared host, so the challenge gets fetched from a machine that knows nothing about it.
When the addresses match, issue:
sudo certbot --nginx \
-d technobabu.com -d www.technobabu.com \
--agree-tos -m you@example.com --no-eff-email
The plugin edits your server block in place, adds the certificate paths, and offers to set up an HTTP-to-HTTPS redirect. Let’s Encrypt stopped sending expiry-warning emails on 4 June 2025, so nothing will email you if renewal breaks. The warning has to come from your own checks: a dry run after every config change and an external days-remaining check, both covered below.
staging first, so you don’t burn rate limits
Let’s Encrypt allows 50 new certificates per registered domain and 5 for the exact same set of names every 7 days, and an account can fail validation for a given name only 5 times an hour. The failure limit is the one debugging hits, because every failed attempt against production counts; it refills at one attempt every 12 minutes. Their staging environment exists for exactly this. It issues certificates from an untrusted test authority, so browsers will complain, but the protocol path is identical and the limits are far looser.
# full dry run against staging, changes nothing permanent
sudo certbot certonly --nginx -d technobabu.com --dry-run
# or issue a real-but-untrusted staging cert to test your config end to end
sudo certbot --nginx -d technobabu.com --test-cert
redirecting HTTP to HTTPS without breaking renewal

Certbot’s own redirect is fine. If you’d rather write it yourself, the shape below works, and Let’s Encrypt follows a plain HTTP-to-HTTPS redirect during validation, so a redirect alone won’t break renewal; serving the ACME path directly just keeps validation working even if the HTTPS server block is misconfigured.
server {
listen 80;
listen [::]:80;
server_name technobabu.com www.technobabu.com;
# keep this reachable, renewals depend on it
location /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}
the renewal timer, and how to test it


Installing certbot from apt gives you a systemd timer that runs twice a day. It checks every certificate on the box and renews anything within thirty days of expiry. The twice-daily schedule looks excessive for a ninety-day certificate, and that’s deliberate: it means a temporary failure has roughly sixty chances to succeed before anything breaks. Certbot 4.0 and later renew once a third of the lifetime remains, which is the same thirty days for a 90-day certificate. Let’s Encrypt plans to cut its default lifetime to 64 days on 10 February 2027 and 45 days by 2028. The timer copes, but any expiry alert tuned for 90-day certificates will need a lower threshold.
# is the timer actually enabled and scheduled?
systemctl list-timers 'certbot*' --no-pager
# simulate the real renewal path without asking for a certificate
sudo certbot renew --dry-run
# what does certbot think it is managing?
sudo certbot certificates
# independent second opinion, straight from the live socket
echo | openssl s_client -connect technobabu.com:443 \
-servername technobabu.com 2>/dev/null \
| openssl x509 -noout -dates -subject
That --dry-run is the single most useful command in this article. It exercises the challenge, the plugin and the config, against staging, without consuming any production quota. Run it after any change to your web server config, because the thing that breaks renewal is almost never certbot. It’s a server block somebody edited months earlier and forgot about.
reload the web server after renewal
A renewed certificate on disk does nothing until the process holding the old one reloads. The nginx plugin handles this, but if you’re using certonly, or fronting containers, you need a deploy hook:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
when issuance fails, it’s usually one of four things

- Port 80 is not reachable. Check your firewall with
sudo ufw status, and check your provider’s separate cloud firewall, which is a distinct thing people forget on AWS and Oracle. Test from outside the box, never from the box itself. - DNS hasn’t propagated, or points elsewhere. A record changed ten minutes ago may still be cached. Wait, then confirm with
digagainst a public resolver rather than trusting your laptop. - Rate limits. If the error mentions too many certificates for that name, stop retrying immediately. Switch to staging, fix the real problem there, and come back when the window resets.
- The challenge file is being intercepted. A WordPress rewrite rule, a security plugin, or an aggressive catch-all location can swallow the ACME path. Fetch it manually with
curland see what comes back before blaming the authority.
Certbot’s own log is more informative than its terminal output, and lives at /var/log/letsencrypt/letsencrypt.log. If it still doesn’t make sense, the habits in my guide to reading server logs will get you to the actual line that matters faster than guessing will.
common mistakes

- Closing port 80 after switching to HTTPS. Renewal needs it. Redirect it, don’t block it, unless you’ve deliberately moved to DNS-01.
- Relying on Let’s Encrypt to email you before expiry. That service ended on 4 June 2025; an external days-remaining check is now the safety net under your automation.
- Copying certificate files somewhere else manually. Serve them from
/etc/letsencrypt/live/, which is a symlink path that always points at the current version. Copies go stale silently. - Debugging against production and hitting rate limits, then hitting the hourly failed-validation limit, or the weekly limit on re-issuing the same names, on the one evening you had time.
- Requesting a certificate for
wwwwhile DNS forwwwdoesn’t exist. Every name in the request must validate, so one missing record fails the whole certificate. - Forgetting certificates that aren’t on the web server: a mail service, a database with TLS, a container that mounted the files at start time and never looked again.
FAQ
is a free certificate weaker than a paid one?
Cryptographically, no. Same algorithms, both chain to roots that browsers trust, and a browser shows both the same way. What money buys is a higher validation tier where the authority verifies your legal entity, plus warranties and a support number. For a site rather than a bank, domain validation with working automation is stronger in practice, because what actually breaks HTTPS is expiry, not mathematics.
can i get a certificate for a server with no public IP?
Yes, with DNS-01. Because validation happens entirely in your DNS zone, the machine itself never needs to accept inbound connections. This is the route to trusted HTTPS on services inside a flat, on the sort of connection where the ISP hands out a shared address. Use a real domain you own, point the records at a private address, and only devices on your own network will resolve to anything useful.
what happens if renewal fails while i’m travelling?
The timer keeps trying twice a day through the last thirty days of validity, so a temporary failure usually fixes itself. Let’s Encrypt no longer sends expiry emails, so if it doesn’t fix itself, nothing tells you. Add an expiry check to whatever watches your servers, so days-remaining shows up on a dashboard instead of an email you’ll read next week.
does certbot work for apps running in containers?
It does, though the neater pattern is to terminate TLS at a reverse proxy on the host and let the containers speak plain HTTP on loopback behind it. That way one certificate serves everything and your application images stay free of TLS concerns entirely. I walk through that arrangement in the guide to deploying an app with Docker and a reverse proxy.
Update history: Let’s Encrypt no longer emails expiry warnings, so this guide now relies on your own checks; the dry-run and deploy-hook commands, rate limits and certificate-lifetime details were also corrected.
Sources
- Let’s Encrypt: challenge types — the authoritative description of HTTP-01 and DNS-01, including the wildcard restriction.
- Let’s Encrypt: rate limits — current issuance limits and why the staging environment exists.
- Certbot user guide — plugin behaviour, renewal hooks, the dry-run flag and log locations.