Repository navigation
fix(ssl): keep failed and pending Let's Encrypt sites on HTTP, wait for DNS-01 propagation and keep the www redirect - #521
Open
mrrobot47 wants to merge 10 commits into
Conversation
… issued When Let's Encrypt registration, authorization or validation failed during `ee site create --ssl=le`, init_le() cleared site_ssl, but www_ssl_wrapper() still wrote the compose file without HTTPS_METHOD=nohttps. nginx-proxy then served the site over HTTPS with the closest matching certificate of another site (a name-mismatch error in browsers), while the site was saved with SSL not enabled. The wrapper now writes the compose file with nohttps, keeps the HTTP-only www redirect and skips the HTTPS redirect when no certificate was issued, and says how to enable SSL once the issue is fixed. Renewals are unchanged: their caller restores the previous certificate, which keeps serving.
…eservers With the Cloudflare DNS solver, EasyEngine waited a fixed 10 seconds after adding the TXT records and then asked Let's Encrypt to validate. Until a new `_acme-challenge.www.<site>` record is served everywhere, a zone with a wildcard record answers that name with the wildcard's target, e.g. the apex's TXT records, so validation of the www name failed with "Incorrect TXT record". The fixed wait is replaced by polling: the nameservers of the zone that holds each record are looked up, and each of their IPv4 addresses is queried directly, without recursion, until all of them serve the expected value, with backoff and a 120 second limit. On timeout the validation is tried anyway, as before, and the records and nameservers still missing are named. Rebuilding a stale order in check() waits the same way. The queries use a small UDP client with an EDNS0 buffer and a TCP retry for truncated answers, so no new dependency is needed.
…eck fails The www check fetches a random file through the www name and follows redirects. On a site that is already redirected to HTTPS but has no matching certificate yet, the chain http://www → http://<site> → https://<site> failed certificate verification, so the check decided www was not pointed at the server, removed the site's www redirect and issued the certificate without it. The check now follows a redirect to HTTPS without verifying the certificate, as Let's Encrypt's HTTP-01 validator does. The random file content still has to come back, which is what proves the name reaches this site.
…in the nameserver queries Every authoritative nameserver of a zone can answer from the anycast site nearest to this server, while Let's Encrypt validates from several regions, so the time the fixed wait used to give is kept as a minimum once the values are served. The queries set RD: authoritative servers ignore it, and a network that redirects port 53 to its own resolver would otherwise only answer from its cache. Pending signal handlers now run after each query.
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
The custom DNS protocol implementation and production SSL failure and recovery paths warrant final human review.
No open issues 🎉
What changed in this PR
This PR improves EasyEngine’s Let’s Encrypt failure recovery and DNS validation, keeping failed certificate setup on HTTP while preserving the www redirect.
Changes:
- Keeps sites HTTP-only after failed certificate setup on create or update, without changing renewal behavior.
- Replaces the fixed DNS wait with TXT propagation polling and a DNS client supporting UDP and TCP fallback.
- Allows www checks through HTTPS redirects with invalid certificates and corrects DNS cleanup messages.
| File | Description |
|---|---|
src/helper/site-utils.php |
Adds authoritative nameserver discovery, TXT polling, and DNS transport and parsing. |
src/helper/Site_Letsencrypt.php |
Tracks published DNS records and waits after rebuilding stale orders. |
src/helper/SimpleDnsCloudflareSolver.php |
Exposes created TXT records and corrects cleanup messages. |
src/helper/class-ee-site.php |
Adds HTTP-only failure recovery, integrates DNS polling, and adjusts www checks. |
Review effort: Balanced
…ts certificate Without a Cloudflare API key, a DNS-01 create or `update --ssl=le` saves the site as Let's Encrypt and returns before any certificate exists, but still wrote the compose file with HTTPS on. nginx-proxy then redirected the site to HTTPS and served it with the closest matching certificate of another site until `ee site ssl-verify` succeeded. Such a site now stays on plain HTTP: `site_ssl` stays `le`, so ssl-verify, ssl-info and delete keep working on it, and a missing certificate marks it as pending (`is_le_ssl_pending()`). www_ssl_wrapper() writes the compose file with `nohttps` for it, and site refresh/enable, the PHP version update and alias domain updates keep `nohttps` while it is pending. Once a standalone `ee site ssl-verify` issues the certificate, it turns HTTPS on (compose file, nginx container, HTTPS www redirect when www points here) and calls the new `site_scheme_changed()` hook, so site types that store the site URL can switch it. `update --ssl=le` on a pending site points to ssl-verify instead of saying SSL is already enabled, and a pending update no longer reports SSL as enabled.
After a failed create, init_le() and the ACME client suggested `ee site ssl-verify`, which can't work there: the site has no SSL then. The site-level messages now come from the caller: a create says to run `ee site update <site> --ssl=le`, `update --ssl=le` says to re-run itself, a renewal keeps the current certificate. A standalone ssl-verify still prints its own retry hint, and the manual DNS-01 "run ssl-verify once the DNS changes have propagated" message is unchanged. A failed `update --ssl=le` printed the create-time "served over HTTP only" warning and then update_ssl()'s own error, and rolled the site back twice. www_ssl_wrapper() now leaves both to update_ssl().
…ificate, and refuse to inherit from it A pending manual DNS-01 site can also get its first certificate from `ee site ssl-renew` or an alias domain update, e.g. once a Cloudflare API key is set. www_ssl_wrapper() then turns HTTPS on, but only a standalone ssl-verify told the site type, so WordPress kept its http URLs. reissue_le_certificate() now calls site_scheme_changed() too when the site was pending before and is issued now. A site can't inherit the certificate of a pending parent: it would get HTTPS without any certificate to serve.
…tch its URLs when HTTPS activation fails after issuance A pending site has no previous certificate for a failed `ssl-renew --force` to keep serving, but www_ssl_wrapper() still turned HTTPS on for it, as for any renewal. It now fails closed when no certificate exists. When ssl-verify issued the certificate but the compose file or the nginx start failed, the HTTPS redirect and site_scheme_changed() were skipped. ssl-verify won't see the site as pending again and the suggested refresh doesn't call the hook, so WordPress kept its http URLs. Both now run before the error.
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved rollback and concurrency issues, alongside the new DNS transport and cross-command certificate transitions, require fixes and final human validation.
3 open issues
2 resolved since last review
Review effort: Balanced
…issuing its first certificate If the container start fails after www_ssl_wrapper() issued a pending site's first certificate during a renewal or alias update, reissue_le_certificate() restores the certificate files, which removes that certificate, but the compose file already served HTTPS. The rollback now writes the HTTP-only compose file again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Pairs with EasyEngine/site-type-wp#247, which keeps the WordPress URLs on http while the site is served over HTTP; both should ship in the same release.
Problem
A site created with
--ssl=leusing the DNS-01 challenge through Cloudflare, in a zone that has a wildcard record (*.<zone> CNAME <zone>), could end up half on HTTPS and later lose its www redirect._acme-challenge.www.<site>is served everywhere, that name matches the wildcard and is answered with the target's TXT records (the apex's), so Let's Encrypt validated the site name but failed on www with "Incorrect TXT record ... found at _acme-challenge.www...".init_le()clearedsite_ssland returned, butwww_ssl_wrapper()still wrote the compose file withoutHTTPS_METHOD=nohttpsand restarted the site. The site was saved with SSL "Not Enabled", while nginx-proxy served it over HTTPS with the closest matching certificate of another name, so browsers showed a name-mismatch error.update --ssl=lealready rolls back throughdisable_ssl(); create had no such path.ee site update <site> --ssl=leran the www check, which follows redirects:http://www.<site>→http://<site>→https://<site>. The last hop failed certificate verification on the wrong certificate, so the check decided www wasn't pointed at the server, removed<site>-redirect.conf, and the new certificate was issued with www but without the HTTPS www redirect._acme-challenge.twice.update --ssl=lesaved the site as Let's Encrypt and returned before any certificate existed, but still wrote the compose file with HTTPS on, so nginx-proxy redirected the site to HTTPS with the closest matching certificate untilee site ssl-verifysucceeded.ee site ssl-verify, which can't work then (the site has no SSL), and a failedupdate --ssl=leprinted overlapping messages.Changes
www_ssl_wrapper()): when the certificate step leavessite_sslempty on create or update, the compose file is written withnohttps, the containers are recreated, the HTTP-only www redirect is kept and the HTTPS redirect is skipped. On create, a warning says the site is served over HTTP only and gives theee site update <site> --ssl=lecommand to run once the issue is fixed. On update,update_ssl()does the rollback (disable_ssl()) and reports the failure once, and the DB keeps the site without SSL. Renewals (ssl-renew, alias changes) are unchanged: their caller restores the previous certificate, which keeps serving over HTTPS.NSrecords whose owner is the name itself, so a wildcard match doesn't count), and each IPv4 address of each nameserver is queried directly without recursion, so no resolver cache is involved. EasyEngine proceeds once every nameserver serves every expected value (a wildcard certificate has two values on the same name), with backoff (2, 4, 8, 16 s) and a 120 s limit. The old 10 seconds are kept as a minimum after that: anycast nameservers answer from the site nearest to this server, while Let's Encrypt validates from several regions. The queries set RD, which authoritative servers ignore, so a network that redirects port 53 to its own resolver still gets answers. On timeout it names the records and nameservers still missing and tries the validation anyway, as before, so the existingssl-verifyhints still apply. Rebuilding a stale order incheck()waits the same way. The queries use a small DNS client insite-utils.php(UDP with an EDNS0 buffer, TCP retry on a truncated answer, ID and response checks), so there's no new dependency, and nothing runs when no record was created.site_sslstaysle, sossl-verify,ssl-infoand delete keep working, and a missing certificate marks the site as pending (is_le_ssl_pending()).www_ssl_wrapper()keeps such a site on HTTP (composenohttps, HTTP-only www redirect) and says it is served over HTTP untilee site ssl-verify <site>succeeds.ee site refresh/enable --refresh,update --phpand alias domain updates keepnohttpswhile it is pending. A standalonessl-verifythat issues the certificate turns HTTPS on (compose file, nginx container, HTTPS www redirect when www points here) and calls the newsite_scheme_changed()hook, which site types that store the site URL override (site-type-wp switches the WordPress URLs). A renewal or alias update that issues the first certificate (e.g. after a Cloudflare key is set) calls it too. A failed renewal of a pending site stays on HTTP (there is no certificate to keep serving), and if HTTPS activation fails after ssl-verify issued the certificate, the redirect and the hook still run, soee site refreshcompletes it.update --ssl=leon a pending site points tossl-verify, a pending update no longer says SSL is enabled, and a site can't inherit the certificate of a pending parent.ee site ssl-verifyhints are gone frominit_le()and from the order/validation failures inSite_Letsencrypt; the caller says how to retry (create:ee site update <site> --ssl=le; update: re-run the update; renewal: the current certificate is kept). A standalonessl-verifystill prints its own retry hint, and the manual DNS-01 "run ssl-verify once the DNS changes have propagated" message is unchanged (now followed by a newline).Testing
leand stays HTTP, update on a pending site points to ssl-verify, a failed update rolls back and reports once without ssl-verify hints, ssl-verify turns HTTPS on with or without the www redirect and calls the hook, a renewal that issues the first certificate calls the hook, a failed renewal of a pending site stays HTTP, a failed HTTPS activation still switches the URLs and the redirect, refresh keeps a pending site on HTTP, a pending inherit parent is refused, authorization failures print no ssl-verify hint); fail-closed matrix (failed create with and without www, wildcard hint, successful create, failed renewal keeps HTTPS, enable untouched, still-valid certificate kept, failed update ends without SSL and without any HTTPS compose write); the www check throughhttp → http → httpswith a self-signed certificate for another name (pointed when the file comes back, not pointed when it doesn't); the DNS poll against a fake authoritative server (wildcard answer first and the record after 3 s, the 10 s minimum, the RD flag, never appearing, a lagging second nameserver, two values on one name, truncated UDP answer retried over TCP, NXDOMAIN, an unreachable server, malformed responses); the Cloudflare solver's created records and cleanup message. The fail-closed, www and cleanup tests fail ondevelop.* CNAME <apex>: nameserver discovery finds both servers, the wildcard answer is rejected, the wait returns once the second server has the record (and not before 10 s), times out when the record never appears or only one server has it, and a 3.6 KB answer falls back to TCP.nohttps, the redirect conf has no HTTPS block,https://<site>gets nginx-proxy's default error instead of the site; thenupdate --ssl=leissues a certificate covering www and the HTTPS www redirect works;update --ssl=le;update --ssl=lethen issues the certificate, switches the URLs to https and redirects http to https (with site-type-wp#247);update --ssl=lepoints to ssl-verify, and once the TXT records are publishedssl-verifyissues the certificate, turns HTTPS on with the www redirect and switches the URLs to https;developthe same three new scenarios fail: the failed create isn't kept on HTTP, and the update of the site already redirected to HTTPS issues the certificate without www.