dnsprobe v1

Let's Encrypt + ACME: the 60-second mental model

· ~3 min read · dnsprobe.net/blog

Before ACME, getting an SSL certificate involved a human at the CA reading an email, calling a phone number on the WHOIS record, or eyeballing a corporate registration document. ACME (Automated Certificate Management Environment, RFC 8555) replaced that with a four-step protocol that Let's Encrypt, ZeroSSL, BuyPass and a handful of other CAs implement. Every ACME client (certbot, acme.sh, lego, Caddy's built-in, Traefik's built-in) speaks the same protocol. Once you understand the four steps, you can debug any of them.

The four phases

  1. Account registration. Client generates a keypair locally. Sends the public key + an email to the CA. CA stores it and returns an account URL. This is your identity at the CA from now on. Lose the private key and you've lost the account.
  2. Order. Client says "I want a certificate for example.com and www.example.com". CA responds with an order containing one authorisation per name.
  3. Authorisation. For each name, CA issues a challenge — a token the client must prove it controls. The client publishes the proof somewhere the CA can verify. CA polls. On verification success, the authorisation is marked valid.
  4. Finalisation. Client generates a CSR (Certificate Signing Request) for the names. CA verifies all authorisations are valid, signs the cert, returns it as a chain.

That is the whole protocol. The interesting variation is in step 3: how the client proves control of the name.

The three challenge types

http-01. CA tells client: "put this token at http://example.com/.well-known/acme-challenge/<token>". CA fetches the URL. If it gets the expected token back, the name is validated. Works only for non-wildcard names; the CA must reach port 80.

tls-alpn-01. CA tells client: "respond to a TLS handshake on port 443 with a special ALPN value carrying this token". CA does a TLS handshake with acme-tls/1 ALPN; client's TLS termination must be aware and respond with a cert containing the token. Common in mesh/service deployments where port 80 is not exposed.

dns-01. CA tells client: "publish a TXT record at _acme-challenge.example.com containing this token". CA queries DNS. Works for wildcard names. Requires the client to have programmatic access to the DNS provider's API.

What goes wrong

Mapping symptoms to root causes:

  • "Connection refused on port 80." Your http-01 challenge fails because the CA cannot reach http://example.com/.well-known/.... Either port 80 is blocked at your firewall, or you don't have a web server listening, or your reverse proxy is redirecting all HTTP to HTTPS too aggressively. The fix is to allow /.well-known/acme-challenge/ through HTTP unredirected.
  • "Invalid response from /.well-known/acme-challenge/<token>: 404." Your client wrote the token to a place the CA can't find. Usually a webroot path mismatch between certbot's --webroot-path and the actual document root.
  • "CAA forbids issuance for this name." Your CAA record does not include letsencrypt.org. See the CAA post for the fix.
  • "DNS problem: SERVFAIL looking up TXT for _acme-challenge." Your DNS provider's API created the record but the authoritative server has not picked it up yet, or DNSSEC is broken. ACME clients have --dns-propagation-seconds-style flags to wait.
  • "Too many certificates already issued for this exact set of domains." Let's Encrypt's rate limit is 5 identical-name-set issuances per week. You are running ACME on every deploy. Cache the cert.

Renewal sanity

Let's Encrypt certs are 90 days. The recommended renewal trigger is 30 days before expiry. ACME clients run on cron or systemd timers; if you've not heard from yours for a while, it is probably dead and your cert is about to expire silently. Set an external monitor on the cert expiry date.

The cert expiry shows up in the SSL panel on dnscheck — try letsencrypt.org itself. Anything closer than 14 days to notAfter should be a hard red.

Useful client flags

# certbot — verbose dry-run (won't burn a real issuance against the rate limit)
certbot certonly --webroot -w /var/www/html -d example.com --dry-run --verbose

# acme.sh — issue with DNS-01 via Cloudflare API
export CF_Token="..."
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'

# lego — DNS-01 via Route53
lego --email [email protected] --dns route53 --domains example.com run

Reference: RFC 8555 (ACME), Let's Encrypt operator docs.