Reported symptoms
web.lab.example is a public marketing site with an ACME client, a
systemd timer and a year of unbroken automatic renewals. Nobody has
thought about its certificate since it was set up.
On 24 August a small change went through. The team had built an
internal admin interface at unreachable.lab.example, and rather
than issue a second certificate they added the new name to the
existing one. One certificate is simpler to manage than two. The
change was approved in four minutes.
At 14:20 UTC on 5 September an engineer opening the site for an unrelated reason notices the padlock detail and reads the expiry date. Nine days.
- The site is up and serving. The certificate is valid. No probe has failed, no alert has fired, and there is no incident open.
- The renewal timer is enabled, its last run is recent, and
systemctl list-timerslooks entirely normal. This is read in the channel as evidence that renewals are healthy. - Running the renewal by hand reproduces the failure immediately. The output ends with a connection error naming a hostname that half the channel does not recognise, because it belongs to the internal admin interface and not to the public site.
- That hostname resolves perfectly from every workstation and every server anybody tries it from, which sends two people looking for a firewall rule.
- Reading back through the journal, the last twelve renewal attempts have all failed the same way, the first of them on 24 August.
The failure has been sitting in a journal for twelve days, correct and complete, and the estate has been monitoring the wrong thing: the expiry date of a certificate that was comfortable at the time, rather than the automation that had already stopped working.
Evidence provided
$ sudo certbot certonly --standalone -d web.lab.example -d unreachable.lab.example Type: connection
Detail: Get "http://unreachable.lab.example:80/.well-known/acme-challenge/XLMIGp112V_...": error
occurred while resolving URL "...": lookup unreachable.lab.example on 127.0.0.11:53: no such host
Hint: The Certificate Authority failed to download the challenge files from the temporary standalone
webserver started by Certbot on port 80. Ensure that the listed domains point to this machine and that it
can accept inbound connections from the internet.Illustrative output
$ sudo certbot certonly --standalone -d web.lab.exampleRequesting a certificate for web.lab.example
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/web.lab.example/fullchain.pem
Key is saved at: /etc/letsencrypt/live/web.lab.example/privkey.pem
This certificate expires on 2026-11-24.Illustrative output
$ dig +norecurse @ns1.lab.example unreachable.lab.example A$ curl -sS -i http://web.lab.example/.well-known/acme-challenge/probe$ openssl x509 -in /etc/ssl/certs/web.lab.example.crt -noout -checkend 7776000Certificate will expireIllustrative output
$ sudo journalctl -u certbot-renew.service --since '2026-08-20' --no-pager | grep -c 'Type: connection'$ sudo grep -E 'Starting [0-9]+ validations|Attempting to validate' /var/log/pebble.logPebble ... Starting 3 validations.
Pebble ... Attempting to validate w/ HTTP: http://web.lab.example:80/.well-known/acme-challenge/70MIGp-...
Pebble ... Attempting to validate w/ HTTP: http://web.lab.example:80/.well-known/acme-challenge/70MIGp-...
Pebble ... Attempting to validate w/ HTTP: http://web.lab.example:80/.well-known/acme-challenge/70MIGp-...Illustrative output
Work the evidence before reading on
The certificate that is nine days from expiry is not the one that broke. Everything interesting in this incident follows from that sentence.
- The failure detail names one hostname. The certificate about to expire carries a different one. Why does a problem with the first stop the second from being renewed?
- The new name resolves from every host anybody tried. The authority says it does not resolve. Both statements are true at once. What makes them compatible?
- The hint suggests checking that the domains point at this machine and that it accepts inbound connections. Read the detail line again and decide whether the hint is describing this failure.
- The staging trace shows three validations for a single authorisation. What does that tell you about how much confidence a successful fetch from your own laptop deserves?
Before continuing: explain why an order containing one unsatisfiable name is worse than an order that simply omits it, and name the check that would have caught this before submission.
Root cause
An internal only name cannot satisfy HTTP-01
HTTP-01 asks the certificate authority to fetch a token over the public internet from whatever address the name resolves to. That is the entire mechanism, and it has one hard prerequisite: the name must resolve publicly to something the authority can connect to.
unreachable.lab.example exists only in the internal DNS view. The
four authoritative nameservers for the public zone return an empty
answer for it. The authority’s detail line says exactly this: the
lookup returned no such host. It never got as far as attempting a
connection, so the hint about inbound connectivity, which is
excellent advice for a different failure, is describing a step that
was never reached.
No firewall change fixes this. There is nothing public to connect to.
An ACME order fails as a unit
An order carries a list of identifiers, and it becomes ready only when every authorisation in it is valid. There is no partial success and no per name fallback.
So adding the internal name did not create a second thing that might fail independently. It made the public name, which had been validating without trouble for a year, depend on a name that could never validate at all. The renewal stopped on 24 August because a name added on 24 August is unsatisfiable, and the certificate that was renewing perfectly has been counting down ever since.
Twelve failures with nobody at the other end
The renewal ran under a timer. Its failures went to the journal, which is exactly where they belong and is not where anybody looks.
The estate did have certificate monitoring, and it was watching the expiry date of the served certificate. On 24 August that date was comfortable, so it said nothing, correctly, for twelve days. The automation had already failed and the only signal that would ever fire was one derived from the consequence rather than the cause.
An alert on remaining validity tells you how long you have. An alert on renewal failure tells you that you have a problem. This estate had the first and not the second, and the first is the one that arrives late by construction.
Resolution
- Measure the remaining validity before doing anything else.
openssl x509 -noout -checkend 604800against the served certificate answers whether you have a week, and that answer sets how carefully you can work. Nine days is enough to be deliberate and not enough to experiment in production. - Stop the renewal timer while you work. Repeated failed validations consume a rate limit of their own, and exhausting it during the window in which you actually need issuance converts a fixable problem into a wait you cannot shorten.
- Unblock the public name before fixing the name that broke it. Reissue
web.lab.exampleon its own, with the internal name removed from the order entirely. This ends the deadline in one step and puts the service back where it was on 23 August. - Do not point the client at the staging authority to make the errors stop, and do not install anything that is not signed by the production authority. A staging certificate converts a problem that is nine days away into an outage that starts at the next reload.
- Decide what the internal interface actually requires, now that nothing is urgent. If it is never reachable from the internet then HTTP-01 cannot validate it under any configuration, so the choice is between DNS-01, which validates through the public authoritative zone and needs the name to exist there, and issuing from the internal certificate authority.
- For an interface that is genuinely internal, the internal certificate authority is the right answer. It removes a public dependency, it removes the rate limits, and it lets you set a validity period that suits the service rather than one set by public policy.
- If the name genuinely should be public, create the record in the public zone first and confirm it resolves from a network with no route into the estate, before submitting another order. Confirming it from inside is the mistake that started this.
- Iterate against the staging environment rather than production while you settle the configuration, and switch to production only once a staging dry run passes end to end.
- Re-enable the timer and let it complete one unattended cycle before closing the incident, since a renewal that only works when you type it is a renewal that will fail again in sixty days.
Verification
- Take the certificate off the wire rather than off the disk. Connect from a host outside the network and read the certificate the server actually presents, then check its
subjectAltNamelist and itsnotAfter. This single observation covers issuance, deployment and reload together, which no file based check can. - Understand why the file is not enough here: the client writes into an archive directory and keeps a live directory of symlinks that are repointed at each renewal, so a correct looking path can happily coexist with a process still holding the previous certificate in memory.
- Confirm the web server actually reloaded by comparing its main process start time against the timestamp of the renewal. A deploy hook that silently did not run is the second most common way an ACME renewal appears to succeed and changes nothing.
- Confirm the order reached a valid state at the authority rather than inferring success from the absence of an error, and confirm no authorisation in it is still pending.
- Prove the automation works unattended with
certbot renew --dry-run, which must report that all simulated renewals succeeded, and then let the timer complete one real cycle on its own schedule. - Verify that a failure would now be noticed. Break a renewal deliberately in the staging configuration and confirm an alert reaches a human within the interval you designed for. The twelve silent failures are the part of this incident most likely to recur wearing a different hat.
- Confirm the internal interface is serving a certificate from whichever authority you chose, and that its own renewal path is scheduled and monitored rather than manual.
Prevention
- Keep public and internal names on separate certificates. The certificate is the unit of failure for an ACME order, so every additional name is another way for the whole thing to stop renewing. One certificate per trust boundary costs a few more renewals and removes the coupling entirely.
- Resolve every name before submitting the order. A pre-flight in the renewal wrapper that queries a public resolver for each requested identifier, and refuses to proceed when one has no public address, is a five second check that would have caught this on 24 August.
- Alert on renewal failure, not only on expiry. Two consecutive failed runs should page regardless of remaining validity, because the automation is what broke and the expiry date is only its eventual symptom. Keep the expiry alert too, warning at 30 days and paging at 7, and drive it from a probe of the served endpoint.
- Test the challenge path from two external networks. The authority validates from several perspectives and requires them to agree, so a fetch from your own laptop is weaker evidence than it looks, and split horizon DNS defeats it completely.
- Run a weekly staging dry run and treat failure as a defect. It exercises the account, the challenge, the plugin and the deploy hook without touching production rate limits, and it fails during office hours instead of at the end of a validity window.
- Refuse to install a certificate from the wrong issuer. Three lines in the deploy hook comparing the issuer against the expected production authority prevents every variant of the staging certificate accident, including the one committed at four in the morning by somebody very tired.