fix(certbot): read the dns-01 challenge from the authoritative nameservers - #1130
Conversation
|
Thanks — this caught a real hole, and #1 in particular is the kind I would rather not have found in production. Pushed #1 — hard
|
The egress data pointYou asked for the one measurement that decides whether #1 is theoretical: a CVM on this host can reach external authoritative nameservers over UDP/53. I deployed a throwaway CVM on the same guest image and VMM config as the gateway, running nothing but
The CVM's So on this host #1 is not the failing case — but that does not make it theoretical. It means this environment happens to have open egress. The scenario you described is a policy choice, not a law, and the change should not depend on which way that choice went on the machine I happened to test. The fallback stands on its own. Two corrections to my own testing, since they nearly produced wrong answersFirst probe was ambiguous and I almost read it backwards. It queried Second probe had a shell-quoting bug. The I am flagging both because a reader skimming the log would see a lot of empty output and could reasonably conclude egress is blocked. It is not. Still outstandingA live issuance through a gateway carrying the review fixes. The image I had building was for |
97c3620 to
73fac0e
Compare
Live A/BRan both arms on the same host within the same hour, changing only the gateway image.
Control: Treatment: Everything else was held constant: same host, same VMM and guest image, same KMS CVM, same Cloudflare zone and credential, same Let's Encrypt staging endpoint, and both names freshly created and never queried before the run. Both arms are fresh runs. I had originally planned to use the earlier 09:04 failure as the control, but that was a different gateway image with different credential and certificate state — close enough to look like a comparison without being one. Re-running it against the current build was worth the extra five minutes. On the fallback pathThe egress probe reported earlier means the treatment arm took the authoritative path, not the fallback — UDP/53 to Cloudflare's nameservers is open from a CVM on this host. So this run demonstrates the fix working, and does not exercise the transport-error fallback that finding #1 was about. That path remains covered by reasoning and by the code review rather than by a live run, and I would rather say so than let a green A/B imply more coverage than it has. Forcing it would mean blocking egress on the host, which I did not want to do on a shared machine. |
|
Fixed both in (a) No backoff and a misleading log after fallbackThe log line is the worse half. Once a domain has fallen back, a subsequent transport error re-enters the same arm and announces "falling back to the system resolver" — while already on the system resolver. Anyone reading that log is being told something untrue about the current state. The missing backoff compounds it: Now the fallback announces itself once, tracked in a (b) Discovery outside the budgetI think this one is closer to a bug than a tradeoff, because of how the two timeouts interact.
That exit is what finding #1 was about: it is the reason a DNS problem degrades to "the ACME server validates from its own view" instead of failing issuance. So leaving discovery outside the budget reintroduces, by a different route, the failure mode #1 closed — and I introduced the discovery phase that makes it reachable.
Not appliedThe trailing-dot FQDN tightening and the CNAME-delegation note — agreed both are minor and neither changes behaviour on dstack's own DNS-01 path. Happy to add them if you would rather they land here than as follow-ups.
One note on the push: the branch had been rebased remotely when #1129 merged and GitHub retargeted the base. I verified the remote tip was content-identical to my local pre-rebase tip ( |
Stacked on #1129 — that PR gets issuance as far as publishing the TXT record; this one is about what happens next. Review after it merges, or read the single-file diff against its branch.
Problem
check_dnsverifies the challenge record it just wrote. On a zone whose SOA minimum exceedsmax_dns_wait, that check can never pass, however long it waits.The record is created and queried moments apart. DNS has not propagated in that window, so the first lookup returns NXDOMAIN — and the recursive resolver caches that negative answer for the zone's SOA minimum. Every retry inside the wait window is then answered from that cache, describing a world where the record does not exist.
Observed on a live gateway issuing for
06rc0.kvin.wang:The record existed the whole time and was globally visible:
The zone's negative TTL is 1800s against a 300s budget:
Rebuilding the
TokioResolvereach iteration — which the loop already did — does not help. That clears hickory's in-process cache; the negative answer lives in the recursive resolver upstream.Fix
Read the challenge record from the zone's authoritative nameservers, so no recursive cache sits in the path.
Discovery walks up a label at a time until a name actually carries NS records, because neither
_acme-challenge.<name>nor the name below it is usually a zone cut — for_acme-challenge.06rc0.kvin.wangthe NS records live onkvin.wang. The walk stops before the public suffix; a TLD's nameservers cannot answer for the record, so falling back beats querying them.The authoritative resolver runs with caching disabled. Its only job is to observe a record written seconds ago, so a cached answer of any age is the wrong answer.
If the nameservers cannot be reached, it warns and falls back to the system resolver rather than blocking issuance — the ACME server has its own DNS view either way, and the existing timeout already proceeds on expiry.
Two things this deliberately does not do:
1.1.1.1, threaded through the DNS credential, the proto and the admin API. That was unnecessary: the bootstrap lookup reads NS records and their addresses, which are stable records where ordinary caching is correct and wanted. Only the challenge record must dodge the cache. Dropping the knob removed 40 lines and left the gateway untouched.Verification
cargo check --workspace --locked,cargo test -p certbot,cargo fmt --check,cargo clippy -p certbot -p dstack-gateway -- -D warningsall pass.A unit test pins the label walk, including the boundary:
06rc0.kvin.wangkvin.wanga.b.example.comb.example.comkvin.wangNone— stop at the registrable namewangNone— never query a TLDThat test exists because the first implementation got this wrong: it looked up NS records on the challenge's own name, which returns NODATA, found nothing, and silently fell back to the recursive resolver. The change would have had no effect while appearing to work.
dig NS 06rc0.kvin.wangreturning only a SOA is what surfaced it.Not yet covered: a live issuance through a deployed gateway. The image was still building when this was opened; I will add the result as a comment. One risk I want to state rather than discover in review — the resolver is configured
udp_and_tcp, so if UDP/53 to external addresses is restricted inside a CVM it should fall back to TCP, but that path has not been exercised on real hardware yet.