Understanding Let's Encrypt: ACME, CT Logs, and the Trust Model
Why this single CA rewired the web
Before Let's Encrypt launched in late 2015, getting a TLS certificate looked like this:
- Generate a CSR.
- Pay a CA somewhere between $30 and $300 a year.
- Click through a manual domain-validation process.
- Wait hours, sometimes days, for issuance.
- Manually deploy the thing.
- Remember to renew it in 12 months. You won't.
The result was predictable. Millions of sites ran on plain HTTP because the friction was too high, and HTTPS became a signal reserved for banks and webmail. Ten years on, HTTPS adoption sits over 95% of global web traffic, up from roughly 40% the year before Let's Encrypt opened its doors. That's not a nudge. That's what happens when you remove every point of friction from a security control — free, automated, on-demand.
Let's get into how it actually works, because the details matter the moment something breaks.
ACME — the protocol that made automation possible
ACME (Automatic Certificate Management Environment, RFC 8555) is the protocol that does the heavy lifting. Let's Encrypt's Boulder is one implementation; the protocol itself is an open standard, and multiple CAs speak it now. Pretty much every automation tool you've used against Let's Encrypt — certbot, acme.sh, cert-manager, lego — is just a different ACME client.
The flow, at a level you can actually debug
Client ACME server (Boulder)
│ │
│── POST /acme/new-account ───────────────────►│ Register account (EC key pair)
│◄─ 201 Created + account URL ─────────────────│
│ │
│── POST /acme/new-order ─────────────────────►│ Request cert for example.com
│◄─ 201 Order + authz URLs ────────────────────│
│ │
│── GET /acme/authz/{id} ─────────────────────►│ Fetch challenge options
│◄─ 200 {http-01, dns-01, tls-alpn-01} ────────│
│ │
│ [Complete chosen challenge] │
│ │
│── POST /acme/challenge/{id} ────────────────►│ Signal readiness
│◄─ 200 Challenge accepted ────────────────────│
│ │
│ [Boulder validates out-of-band] │
│ │
│── POST /acme/order/finalize ────────────────►│ Submit CSR
│◄─ 200 Certificate URL ───────────────────────│
│ │
│── GET /acme/cert/{id} ──────────────────────►│ Download certificate chain
│◄─ 200 PEM chain ─────────────────────────────│
The important bit: the CA never sees your private key, only your CSR. That account key pair at the top is how the CA knows you are the same entity across calls.
The three challenge types, and which one to pick
HTTP-01. You place a token at http://<domain>/.well-known/acme-challenge/<token>. The CA fetches it over plain HTTP. Token matches the client's key authorisation, challenge passes.
# certbot HTTP-01, standalone mode
certbot certonly --standalone -d example.com
Catch: won't do wildcards, and needs port 80 open and not hidden behind a CDN that caches 404s cheerfully.
DNS-01. You drop a TXT record at _acme-challenge.example.com containing the token. CA does a DNS lookup. Done.
# certbot with Route53
certbot certonly --dns-route53 -d "*.example.com"
This is the one I reach for by default. Works for wildcards. Works for internal hostnames — the domain needs public DNS, but the host doesn't need to be reachable from the internet. Works through CDNs. The trade-off is that your automation now needs credentials to edit DNS, which is a bigger blast radius than "permission to touch a webroot." Scope those credentials carefully.
TLS-ALPN-01. You present a specially crafted self-signed cert during a TLS handshake on port 443 using the acme-tls/1 ALPN extension. Used by cert-manager and Traefik. Nice for containers where binding port 80 is awkward.
Certificate Transparency — the quiet revolution nobody talks about
CT (RFC 6962) is the most important structural change to the CA ecosystem this century, and it's still under-discussed. If you operate PKI at any real scale, you need to understand it.
The problem it kills
CAs can mis-issue certs. Sometimes through honest errors, sometimes through compromise. Before CT, a rogue or compromised CA could issue a cert for google.com, hand it to an attacker, and Google wouldn't know. There was no public record. The trust model leaked at the seams and nobody could see it happening.
How it works
Every publicly-trusted cert now has to appear in at least two publicly-auditable CT logs (browser root programs enforce this). The log is an append-only Merkle tree — tamper-evident, verifiable, replicable:
Root hash
/ \
Hash(0,1) Hash(2,3)
/ \ / \
Hash(cert0) Hash(cert1) Hash(cert2) Hash(cert3)
When the CA issues a cert, it submits it to a log and gets back a Signed Certificate Timestamp (SCT). The SCT rides inside the cert itself (or is delivered via a TLS extension). Browsers check it before trusting. No SCT, no trust.
What this means operationally:
- Every cert you issue for your domain is publicly visible on
crt.sh,certspotter, and friends. - Every cert anyone else issues for your domain is also visible — including fraudulent ones.
- You can subscribe to CT monitors and get alerted the moment a new cert shows up for one of your domains.
# Quick crt.sh sweep for your domain
curl -s "https://crt.sh/?q=example.com&output=json" \
| jq '.[] | {name_value, not_before, issuer_name}'
Wire up a CT monitor today
If you take one operational lesson from this article, take this one. Tools worth knowing:
- certspotter — open source, self-hostable. Watches CT logs, emails or webhooks on new issuance for your domains.
- crt.sh RSS — subscribe to
https://crt.sh/atom?q=example.com. Zero infra. - Facebook CT monitoring — free for domain owners, decent UI.
Any cert you didn't issue is an incident. Investigate it immediately. An attacker with a legitimately-signed cert for your domain is a very bad day you will otherwise find out about from a customer.
The chain of trust, laid bare
ISRG Root X1 (self-signed, 4096-bit RSA, baked into browser/OS trust stores)
└── Let's Encrypt R10 (intermediate, 2048-bit RSA)
└── your certificate (ECDSA P-256 or RSA 2048)
Why an intermediate at all? Because root CAs are kept offline in HSMs, usually in secure facilities with ceremonies involving people wearing suits. Intermediates are the ones online, actually signing things. If an intermediate is compromised, you revoke it and issue a new one from the root. If a root is compromised, you get the worst week of your career and every trust store on the planet has to update. The intermediate is disposable on purpose.
ISRG Root X2 is the newer ECDSA (P-384) root — smaller, faster. Not yet universally trusted (older Androids don't carry it), so Let's Encrypt cross-signs its ECDSA intermediate with the RSA root. Backward compatibility, the eternal PKI tax.
The September 2021 outage — worth a paragraph on its own. Early Let's Encrypt certificates were cross-signed by IdenTrust's DST Root CA X3, which expired on schedule. Devices running OpenSSL older than 1.0.2 — mostly older Android phones and a pile of embedded systems — didn't handle the expired intermediate gracefully and just broke. It was a reminder that trust chains expire too, and the failure mode lands hardest on the devices you can't update. Plan for it.
Rate limits, because abuse exists
Let's Encrypt is a free service used by half the internet. They enforce rate limits because otherwise one scripted loop would DoS Boulder into the ground.
| Limit | Value |
|---|---|
| Certificates per registered domain | 50 / week |
| Duplicate certificates | 5 / week |
| Failed validations | 5 / account / hour |
| New orders | 300 / 3 hours |
| Pending authorisations | 300 / account |
Hitting these in production is painful. Hitting them in staging is free. Every ACME client has a staging flag (--staging for certbot). Use it. Always test against staging first, especially anything involving new domains, new challenge types, or automation you haven't run end-to-end before.
Running Let's Encrypt at scale without losing sleep
A handful of operational notes that save real pain:
- Many servers, one cert. Use DNS-01 with a wildcard. Issue once, distribute via your secrets manager. Don't have every server talk to the CA — that's how you discover rate limits the fun way.
- Kubernetes? Use cert-manager. It handles issuance, renewal, and distribution. Configure once, stop thinking about certs. This is one of those rare tools that actually delivers on "set it and forget it."
- Renew at 60 days, reload the right thing. certbot's
--renew-hookruns after a successful renewal. Reload nginx, copy the files, notify the load balancer — whatever your stack needs.
certbot renew \
--renew-hook "systemctl reload nginx" \
--deploy-hook "cp /etc/letsencrypt/live/example.com/fullchain.pem /etc/nginx/certs/"
- Monitor your own certs. Don't rely on certbot to be the first to notice a failed renewal. Prometheus or Datadog or Nagios, doesn't matter — just get an alert when expiry drops below 14 days, from an independent system.
Let's Encrypt is a genuinely remarkable piece of engineering: a free, automated, globally-distributed CA that turned HTTPS from a luxury into a default. Knowing how it works means you can operate it reliably and, more importantly, recognise when the trust model it gives you is enough — and when you need something more.
Stay in the loop
New articles on AI, Cybersecurity, and PKI — delivered to your inbox.