Skip to content

Commit cd77ce0

Browse files
committed
test(gateway): issue a real dns-persist-01 certificate in the e2e suite
The phase added a moment ago stopped short of the CA, because the pinned Pebble image could not serve the challenge -- its binary carried dns-01, http-01, tls-alpn-01 and dns-account-01, and neither "dns-persist-01" nor "_validation-persist". Upstream Pebble has implemented the draft since (va.validateDNSPersist01, parsing the record as an RFC 8659 issue-value), so the image is rebuilt from upstream v2.10.1 with only the `-http` flag this harness has always needed, and the tag is pinned rather than floating on :latest. The flag now lives at github.com/kvinwang/pebble branch http-flag instead of in an unrecorded working tree. So the challenge is covered end to end: selected out of the authorization, posted ready, finalized, and the certificate fetched -- for a domain the gateway holds no credential for. The DNS answer is the only part standing in, since nothing publishes the record here and Pebble runs with PEBBLE_VA_ALWAYS_VALID. certbot warns and proceeds by design, because the CA's DNS view is not this node's. Two orderings the suite has to respect, both learned by getting them wrong: - Every record names the ACME account, so `required_dns_records` is empty until one exists, and in this harness issuing is what creates it. The record assertion therefore runs after issuance, not before. - `renew_timeout` bounds the DNS self-check at half its value. Left at the default the order would sit for 150s waiting on a record that never arrives, so the harness sets 60s and the poll outlasts it. `issuer_domain_name` is set to pebble.letsencrypt.org: a record naming a CA the challenge does not list is ignored, and Pebble does not answer to letsencrypt.org.
1 parent 7cad283 commit cd77ce0

2 files changed

Lines changed: 56 additions & 17 deletions

File tree

dstack/gateway/test-run/e2e/docker-compose.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ services:
5151
timeout: 3s
5252
retries: 5
5353

54-
# Pebble - Let's Encrypt test server (custom build with HTTP support)
54+
# Pebble - Let's Encrypt test server. Upstream plus a `-http` flag, so a
55+
# client in another container can reach it by service name without trusting
56+
# the Pebble root: github.com/kvinwang/pebble branch http-flag.
5557
pebble:
56-
image: kvin/pebble:latest
58+
image: kvin/pebble:v2.10.1-http
5759
container_name: pebble
5860
command: ["-http", "-dnsserver", "172.30.0.10:53"]
5961
networks:

dstack/gateway/test-run/e2e/test.sh

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,15 @@ PEBBLE_DIR="http://pebble:14000/dir"
3333
# Certificate domains to test (base domains, certs will be issued for *.domain)
3434
CERT_DOMAINS="test0.local test1.local test2.local"
3535

36-
# A dns-persist-01 domain. Deliberately outside CERT_DOMAINS: no certificate is
37-
# expected for it, because the Pebble image this harness pins does not implement
38-
# draft-ietf-acme-dns-persist-01 (its binary carries dns-01, http-01, tls-alpn-01
39-
# and dns-account-01, and neither "dns-persist-01" nor "_validation-persist").
40-
# What is asserted here is everything up to the CA: that such a domain is
41-
# accepted with no DNS credential, that the record an operator must publish is
42-
# rendered, that the challenge survives an edit, and that certbot never writes
43-
# DNS for it. Issuance itself needs a Pebble built with the draft.
36+
# A dns-persist-01 domain. Kept out of CERT_DOMAINS because the certificate
37+
# phases check TLS through the proxy, which needs an app behind the domain;
38+
# here the certificate is checked through the admin API instead.
4439
PERSIST_DOMAIN="persist0.local"
4540

41+
# Pebble answers to this name, not to letsencrypt.org, and a validation record
42+
# naming a CA the challenge does not list is ignored.
43+
PERSIST_ISSUER="pebble.letsencrypt.org"
44+
4645
# Cloudflare mock settings
4746
CF_API_TOKEN="test-token"
4847
CF_API_URL="http://mock-cf-dns-api:8080/client/v4"
@@ -175,11 +174,36 @@ test_persist_challenge_survives_an_edit() {
175174
echo "$info" | grep -q "dns-persist-01"
176175
}
177176

177+
# The point of the whole exercise: a certificate is issued for a domain the
178+
# gateway holds no credential for. The record is not published anywhere in this
179+
# harness, so the pre-order self-check cannot pass -- it is advisory by design,
180+
# and certbot warns and proceeds because the CA's DNS view is not this node's.
181+
# Pebble runs with PEBBLE_VA_ALWAYS_VALID, so it accepts.
182+
#
183+
# That leaves the challenge itself covered end to end -- selected out of the
184+
# authorization, posted ready, finalized, fetched -- and the DNS answer the only
185+
# part standing in.
186+
test_persist_domain_issues_a_certificate() {
187+
admin_post RenewZtDomainCert \
188+
'{"domain": "'"${PERSIST_DOMAIN}"'", "force": true}' > /dev/null 2>&1 || true
189+
190+
# The renewal outlives the request that triggered it, and spends the
191+
# advisory DNS wait -- half of renew_timeout -- before it reaches the CA.
192+
local i=0
193+
while [ $i -lt 60 ]; do
194+
if admin_post GetZtDomain '{"domain": "'"${PERSIST_DOMAIN}"'"}' \
195+
| grep -q '"has_cert":true'; then
196+
return 0
197+
fi
198+
sleep 2
199+
i=$((i + 1))
200+
done
201+
return 1
202+
}
203+
178204
# The claim the whole challenge exists for: certbot reads DNS and never writes
179205
# it, so nothing for this domain may ever reach the provider API.
180206
test_persist_domain_never_touches_the_provider() {
181-
admin_post RenewZtDomainCert \
182-
'{"domain": "'"${PERSIST_DOMAIN}"'", "force": true}' > /dev/null 2>&1 || true
183207
local records
184208
records=$(curl -sf "${MOCK_CF_API}/api/records" 2>/dev/null || echo "[]")
185209
! echo "$records" | grep -q "${PERSIST_DOMAIN}"
@@ -236,12 +260,21 @@ test_proxy_tls_health() {
236260
setup_certbot_config() {
237261
log_info "Configuring certbot via Admin API..."
238262

239-
# Set ACME URL
240-
log_info "Setting ACME URL: ${ACME_URL}"
263+
# Set ACME URL and the name this CA is known by. The issuer name is what a
264+
# dns-persist-01 record has to carry, and it is also what CAA names for
265+
# dns-01, so it is one setting for the whole deployment.
266+
# renew_timeout also bounds the pre-order DNS self-check, which is capped at
267+
# half of it. The default 300s would have a dns-persist-01 order sit for
268+
# 150s waiting on a record this harness never publishes.
269+
log_info "Setting ACME URL: ${ACME_URL} (issuer ${PERSIST_ISSUER})"
241270
if ! curl -sf -X POST "${GATEWAY_ADMIN}/prpc/Admin.SetCertbotConfig" \
242271
-H "${ADMIN_AUTH_HEADER}" \
243272
-H "Content-Type: application/json" \
244-
-d '{"acme_url": "'"${ACME_URL}"'"}' > /dev/null; then
273+
-d '{
274+
"acme_url": "'"${ACME_URL}"'",
275+
"issuer_domain_name": "'"${PERSIST_ISSUER}"'",
276+
"renew_timeout_secs": 60
277+
}' > /dev/null; then
245278
log_error "Failed to set certbot config"
246279
return 1
247280
fi
@@ -412,10 +445,14 @@ main() {
412445
log_phase 10 "dns-persist-01 without a DNS credential"
413446
run_test "Domain accepted with no DNS credential" \
414447
"$(test_persist_domain_needs_no_credential; echo $?)"
415-
run_test "Required validation record is reported" \
416-
"$(test_persist_domain_reports_its_record; echo $?)"
417448
run_test "Challenge survives an edit that omits it" \
418449
"$(test_persist_challenge_survives_an_edit; echo $?)"
450+
run_test "Certificate issues without a DNS credential" \
451+
"$(test_persist_domain_issues_a_certificate; echo $?)"
452+
# After issuance: every record names the ACME account, so there is nothing
453+
# to report until one exists, and issuing is what creates it here.
454+
run_test "Required validation record is reported" \
455+
"$(test_persist_domain_reports_its_record; echo $?)"
419456
run_test "No DNS record is ever written for it" \
420457
"$(test_persist_domain_never_touches_the_provider; echo $?)"
421458

0 commit comments

Comments
 (0)