A site moved to a new host. One piece stayed behind, and the new front end proxies a single path back to it. That backend still had a Let's Encrypt certificate, and it could never renew again. HTTP-01 and TLS-ALPN-01 both require the hostname to resolve at the machine being validated, and it now resolves somewhere else entirely. There was a hard expiry date on the calendar and a contact form that would break on it. The fix was not a cleverer ACME challenge.
Work through them, because the answer is structural rather than a configuration problem:
/.well-known/acme-challenge/ on the hostname
being validated. That hostname now resolves to the new front end, so the token would be requested from a
machine that does not have it.DNS-01 would have worked. That is worth saying plainly, because it is the answer most people reach for. It also means minting a token, storing it, rotating it, and accepting that a renewal now depends on a third-party API being up and a credential still being valid.
Here is the question that changes the answer. Who is the audience for this certificate?
Not a browser. Not the public. This certificate exists so that one server you own can verify another server you own before proxying a request to it. It is a machine-to-machine hop across your own infrastructure, and no human ever sees it.
A public CA exists to vouch for strangers to strangers. There are no strangers in this hop. Using one here means both ends depend on an external service, a public trust store, and a challenge mechanism that only works while DNS happens to be arranged a particular way. Every one of those is a dependency bought for no benefit.
This surprises people, so it is worth being precise. Before the change, the front end verified the backend's certificate against the public trust store, meaning any publicly trusted CA could have issued a certificate for that name and been accepted.
After the change it trusts exactly one CA, yours:
backend.example.com {
tls_trusted_ca_certs /etc/ssl/internal-ca-root.pem
}
That is a smaller trust set than a public CA gives you, not a bigger one. "Self-signed is less secure" is a reasonable instinct for a public website and exactly backwards for a hop between two machines you control.
Do not swap the certificate and the trust setting at the same time. Widen trust first, move the certificate, then narrow trust. Nothing is ever untrusted at any point:
Verify at each step rather than at the end. If step three breaks something, you want to know it was step three.
tls directive naming your certificate files. That
line is load-bearing. Without it the automation reasserts itself on the next reload and starts failing ACME
orders for a name it can never validate.
A renewal script that has never renewed anything is a hypothesis. Delete the certificate to force the "no existing certificate, issue one" branch, then watch the whole loop run:
# force the issue path, then confirm the full chain end to end
step ca certificate backend.example.com /tmp/t.crt /tmp/t.key --force
curl -s -o /dev/null -w '%{http_code}\n' https://frontend.example.com/api/health
Check the other sites on the same proxy afterwards too. A reload touches all of them, and a renewal that quietly breaks an unrelated site is a worse outcome than the expiry you were preventing.