Free HTTPS with Let’s Encrypt: Certificates That Renew Themselves

Free HTTPS with Let's Encrypt: Certificates That Renew Themselves

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

Comparison: a TLS certificate encrypts traffic, and proves only that the requester controlled the domain when it was issued.
A Let’s Encrypt certificate says nothing about who runs the site, only who controlled the name.

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

Flow of ACME issuance: generate a key pair, request names, receive a challenge, the authority verifies it, and the certificate is issued.
ACME took humans out of issuance, which is what makes 90-day certificates practical.
Table comparing HTTP-01 and DNS-01: what each checks, port 80 needs, wildcard support, CGNAT, setup effort and common failures.
HTTP-01 for a normal public server; DNS-01 for wildcards or anything behind CGNAT.

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.

ConcernHTTP-01DNS-01
What it checksA file on your web rootA TXT record in your zone
Needs inbound port 80Yes, for issue and renewalNo
Wildcard certificatesNot possibleThe only way
Works behind CGNATNoYes
Setup effortAlmost noneA DNS API token, scoped to the one zone if your DNS host allows it, stored in a root-only file (chmod 600)
Main failure causeFirewall, or a rewrite rule that serves something other than the tokenPropagation delay, wrong zone

issuing your first certificate with certbot

Terminal: install certbot, then dig and curl ifconfig.me both return 203.0.113.42, confirming DNS points at the server.
Compare the two addresses before every first issuance.
Three Let's Encrypt limits: 50 certificates per domain per week, 5 duplicates per week, 5 failed validations per name per hour.
The failed-validation limit is the one you hit while debugging against production.

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

Flow: a request on port 80 for the ACME challenge path is served from the web root; every other path gets a 301 to HTTPS.
Serving the ACME path directly keeps renewal working even if the HTTPS block is broken.

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

Timeline: certificate issued at day 0, renewal starts at day 60, about sixty twice-daily attempts follow, and expiry at day 90 only if all fail.
The timer gives a broken renewal weeks of chances before anyone sees a warning.
Terminal: list the certbot timer, run renew --dry-run with simulated renewals succeeding, list certificates, and read the live certificate dates with openssl.
Run the dry run after every web server config change. That is what usually breaks renewal.

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

Four causes of failed issuance: port 80 blocked, DNS not propagated, rate limits, and a rewrite rule intercepting the challenge file.
Certbot’s log file says more than its terminal output.
  1. 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.
  2. DNS hasn’t propagated, or points elsewhere. A record changed ten minutes ago may still be cached. Wait, then confirm with dig against a public resolver rather than trusting your laptop.
  3. 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.
  4. 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 curl and 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

Checklist of mistakes: closing port 80, relying on expiry emails, copying certificate files, and forgetting certificates outside the web server.
Each of these turns automation back into a manual 90-day timer.
  • 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 www while DNS for www doesn’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