Certificate management used to be an annual event with a diary reminder attached. Somebody bought a certificate, generated a CSR, pasted things into a web form, waited for an email, and installed the result. It went wrong roughly once a year, usually at a weekend.
ACME automated all of that away, and in doing so replaced one well-understood annual failure with several less-understood continuous ones. This article covers how the automation actually works, which validation method to choose and why, and the DNS records that control the whole thing.
What ACME does
ACME — Automatic Certificate Management Environment, RFC 8555 — is a protocol between your server and a certificate authority. Your client generates an account key, asks for a certificate covering some hostnames, and the CA responds with a challenge: prove you control these names. You satisfy it, the CA verifies it, and you get a certificate. The client repeats the exercise before it expires.
Let’s Encrypt issues certificates with a 90-day lifetime, and clients renew well before expiry — 30 days remaining is the conventional trigger, which gives a full month of daily retries before anything is user-visible. That margin is the point. A 90-day certificate with a 60-day renewal cadence tolerates a fortnight of broken automation; a 12-month certificate renewed by hand tolerates nothing. Lifetimes across the industry are trending shorter, so anything depending on manual renewal is on a path to becoming unworkable.
The three challenge types
HTTP-01 is the default and the simplest. The CA gives your client a token; the client writes a file at http://example.com/.well-known/acme-challenge/<token>; the CA fetches it over plain HTTP on port 80.
It requires port 80 to be open and reachable from the internet, and it requires the hostname to already resolve to the server requesting the certificate. Both constraints matter. It cannot issue wildcards.
The most common HTTP-01 failure is a redirect. A server configured to redirect all HTTP traffic to HTTPS will redirect the challenge request too. The CA does follow redirects, so this often works — until the redirect lands somewhere the challenge file is not, such as a www canonical host or a CDN. If renewals fail on exactly the hosts with the tidiest redirect configuration, this is why. The fix is an exception for /.well-known/acme-challenge/:
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
^~ stops nginx evaluating regex locations once this prefix matches, which is what keeps a later location ~ \.php$ block from stealing the request.
DNS-01 proves control by publishing a TXT record:
_acme-challenge.example.com. IN TXT "gfj9Xq...Rg85nM"
It is more work to set up, because your client needs API credentials for your DNS provider, and it is strictly more capable:
- It is the only method that can issue wildcard certificates.
- It works when the hostname does not yet point at the server — which makes it the right choice during a migration, so the certificate is ready before the cutover.
- It works for servers with no inbound access at all, including internal hosts.
The credential scope is the thing to get right. A DNS provider API token that can edit every record in every zone, sitting on a web server, is a significant liability. Most providers now support scoped tokens; restrict to the specific zone and, where offered, to TXT records only.
TLS-ALPN-01 performs the validation inside a TLS handshake on port 443 using a special ALPN protocol identifier. It is useful when port 80 is unavailable, and it is largely the preserve of load balancers and terminating proxies that implement it natively.
Wildcards: when they help and when they hurt
A wildcard certificate for *.example.com covers a.example.com and b.example.com, and does not cover example.com itself or a.b.example.com. One level, no apex. Almost every wildcard certificate therefore needs both names:
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'
Quote the wildcard, or the shell will try to expand it.
Wildcards are the right answer when you have a large and changing set of subdomains — per-client staging hostnames, tenant subdomains, ephemeral preview environments — where issuing individually would be unmanageable or would hit rate limits.
They are the wrong answer for a handful of stable hostnames. One key covering every subdomain means one compromised server can impersonate all of them, and revocation affects everything at once. Individually issued certificates contain the blast radius.
There is also a visibility trade-off. Individually issued names appear in Certificate Transparency logs, which is how you find out that someone has issued a certificate for a hostname you did not expect. A wildcard hides all of that behind a single entry — convenient for privacy, unhelpful for detection.
CAA: controlling who may issue
A CAA record tells certificate authorities whether they are permitted to issue for your domain. CAs are required to check it and to refuse if they are not listed.
example.com. IN CAA 0 issue "letsencrypt.org"
example.com. IN CAA 0 issuewild ";"
example.com. IN CAA 0 iodef "mailto:security@example.com"
Reading those: only Let’s Encrypt may issue; no CA may issue wildcards (";" is the explicit deny value); and report policy violations to that address.
The first field is the flags byte. 0 is the normal value. 128 sets the critical bit, meaning a CA that does not understand the property tag must refuse to issue — rarely what you want.
Two behaviours worth knowing. CAA is checked at the point of issuance, so adding a record does not invalidate existing certificates. And the lookup walks up the tree: if there is no CAA at www.example.com, the CA checks example.com, then com. A record at the apex therefore covers the whole domain unless a subdomain overrides it.
The issuewild line above is a genuinely useful control. If you never issue wildcards, denying them explicitly closes off a whole class of mis-issuance, and costs nothing.
CAA is one of the few security records that is both trivially cheap and almost never present. If you set nothing else this week, set this.
Rate limits and the staging environment
Certificate authorities impose rate limits per registered domain and on duplicate certificates. The specific numbers change; check the current published limits rather than working from memory.
What matters operationally is the failure mode. A misconfigured renewal loop that fails, retries immediately and fails again will exhaust the limit and then be locked out — including for the certificate genuinely about to expire. Every mainstream client has sensible backoff; the danger is homegrown automation, or a container that reissues on every start.
Use the staging environment while developing automation. Let’s Encrypt’s staging endpoint issues from an untrusted root with far more generous limits.
certbot certonly --dry-run --webroot -w /var/www/acme -d example.com
--dry-run exercises the whole renewal path against staging without touching your live certificates or your rate limit budget. Run it after any change to the web server configuration, not only when something has broken.
The step after issuance
Obtaining a certificate is not the same as serving it. Every client has a hook for the reload, and it is the single most common gap in an otherwise working setup — a certificate that renews correctly on disk while the running process continues to serve the old one until it is restarted for some unrelated reason.
With acme.sh, the reload command is bound at install time:
acme.sh --install-cert -d example.com \
--key-file /etc/ssl/private/example.com.key \
--fullchain-file /etc/ssl/certs/example.com.crt \
--reloadcmd "systemctl reload nginx"
Verify what is actually being served, rather than what is on disk:
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -subject
-servername sets SNI. Without it you will be shown the default virtual host’s certificate, which on a multi-site server is a different certificate entirely — and a source of considerable confusion.
Server configuration worth getting right
Support TLS 1.2 and 1.3. Disable TLS 1.0 and 1.1; both are deprecated and no current client needs them.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_prefer_server_ciphers off looks wrong and is deliberate: modern clients order their cipher preferences sensibly and know more about their own hardware than the server does. This matches Mozilla’s intermediate recommendation, which is the configuration to start from unless you have a specific reason to deviate.
Serve the full chain, not just the leaf certificate. A missing intermediate produces the most irritating class of TLS bug — working in every desktop browser, because they cache intermediates from previous sites, and failing in curl, in mobile apps, and in whatever server-to-server integration the client depends on. Point your configuration at the fullchain file, never the cert file.
HSTS, and why to be careful
HTTP Strict Transport Security tells browsers to refuse plain HTTP for a domain, for a stated period.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
The mechanism is one-way. Once a browser has seen that header, it will not make a plain HTTP request to the domain until max-age elapses — even if you remove the header, even if TLS is broken. Setting max-age=31536000 is a commitment to serving valid HTTPS for a year.
includeSubDomains extends that to every subdomain, including ones that do not exist yet. If some internal or legacy hostname under the domain is HTTP-only, this will break it for everyone who has visited the parent, with no obvious cause.
The safe rollout is the same shape as a DMARC rollout: start with a short max-age such as 300, confirm nothing breaks, and increase in stages to a day, a week, a year.
preload goes further still — it opts the domain into a list compiled into browsers themselves, so that even a first-ever visit is HTTPS-only. Submission requires max-age of at least 31536000, includeSubDomains, and the preload directive. Removal from the list is possible but slow, measured in browser release cycles. Do not add the preload token casually; its presence is treated as consent to be submitted.
FXRM issues and renews certificates automatically across our hosting platform, including wildcards where they are needed. See our hosting →