Skip to content

Commit 8a180eb

Browse files
author
Kevin Wang
committed
fix(dstack-ingress): serialize bootstrap with the renewal daemon and let the gateway's DNS cache expire
1 parent 4942bc9 commit 8a180eb

3 files changed

Lines changed: 48 additions & 11 deletions

File tree

custom-domain/dstack-ingress/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ environment:
182182
| `TLSALPN_PORT` | `9443` | Loopback port lego's ACME responder binds to |
183183
| `TLS_TERMINATE_PORT` | `9444` | Loopback port the TLS frontend moves to in tls-alpn-01 mode |
184184
| `RENEW_DAYS_BEFORE` | lego default | Days of remaining lifetime that trigger renewal |
185+
| `DNS_SETTLE_SECONDS` | `30` | Wait after DNS verifies, so the gateway's own TXT cache expires |
185186
| `MAXCONN` | `4096` | HAProxy max connections |
186187
| `TIMEOUT_CONNECT` | `10s` | Backend connect timeout |
187188
| `TIMEOUT_CLIENT` | `86400s` | Client-side timeout (24h for long-lived connections) |

custom-domain/dstack-ingress/scripts/entrypoint.sh

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,18 @@ process_domain_tlsalpn() {
528528
exit 1
529529
fi
530530

531+
# Public DNS being correct is not the same as the gateway acting on it. The
532+
# gateway resolves the app-address TXT itself and caches the answer for the
533+
# record's TTL, so right after the value changes -- which is every time the
534+
# CVM instance is replaced, since the record names the instance -- it can
535+
# still route the challenge to the previous target. Observed exactly that:
536+
# dnsguide passed, then the CA got "Error getting validation data" because
537+
# the gateway was still sending it to the old instance.
538+
if [ "${DNS_SETTLE_SECONDS:-30}" -gt 0 ]; then
539+
echo "Waiting ${DNS_SETTLE_SECONDS:-30}s for the gateway's DNS cache to expire"
540+
sleep "${DNS_SETTLE_SECONDS:-30}"
541+
fi
542+
531543
renew-certificate.sh "$domain"
532544
}
533545

@@ -591,18 +603,30 @@ ensure_placeholder_certs() {
591603

592604
# Obtain certificates once the proxy is already serving, then swap them in.
593605
bootstrap_and_reload() {
594-
# Nested subshell so an `exit 1` from a process_domain helper is reported as
595-
# a failed condition here rather than silently killing this background job.
596-
if ( bootstrap ); then
597-
build-combined-pems.sh || echo "Combined PEM build failed" >&2
598-
if [ -f /var/run/haproxy/haproxy.pid ]; then
599-
kill -USR2 "$(cat /var/run/haproxy/haproxy.pid)" &&
600-
echo "Certificates installed and HAProxy reloaded"
606+
local attempt=0 delay
607+
while true; do
608+
attempt=$((attempt + 1))
609+
# Nested subshell so an `exit 1` from a process_domain helper is
610+
# reported as a failed condition here rather than silently killing
611+
# this background job.
612+
if ( bootstrap ); then
613+
build-combined-pems.sh || echo "Combined PEM build failed" >&2
614+
if [ -f /var/run/haproxy/haproxy.pid ]; then
615+
kill -USR2 "$(cat /var/run/haproxy/haproxy.pid)" &&
616+
echo "Certificates installed and HAProxy reloaded"
617+
fi
618+
return 0
601619
fi
602-
else
603-
echo "Bootstrap failed; the proxy keeps serving the placeholder certificate." >&2
604-
echo "Fix the DNS records above; the renewal daemon retries every 12 hours." >&2
605-
fi
620+
621+
# Falling through to the 12-hour renewal daemon would be a terrible
622+
# retry interval for a flow whose normal state is "waiting for the
623+
# operator to create a DNS record". Back off, but keep trying.
624+
delay=$((60 * attempt))
625+
[ "$delay" -gt 1800 ] && delay=1800
626+
echo "Bootstrap attempt ${attempt} failed; the proxy keeps serving the" >&2
627+
echo "placeholder certificate. Retrying in ${delay}s." >&2
628+
sleep "$delay"
629+
done
606630
}
607631

608632
# Credentials are now handled by certman.py setup

custom-domain/dstack-ingress/scripts/renewal-daemon.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
#!/bin/bash
22

33
while true; do
4+
# Never run concurrently with the initial bootstrap. Under tls-alpn-01 the
5+
# proxy has to be listening before the first certificate can be issued, so
6+
# bootstrap runs in the background while this daemon is already up. Two
7+
# ACME clients at once fight over the challenge port ("bind: Address
8+
# already in use") and, worse, the loser still spends one of Let's
9+
# Encrypt's five failed validations per hostname per hour.
10+
if [ ! -f /.bootstrapped ]; then
11+
echo "[$(date)] Waiting for bootstrap to finish before checking renewals"
12+
sleep 30
13+
continue
14+
fi
15+
416
echo "[$(date)] Checking for certificate renewal"
517

618
all_domains=$(get-all-domains.sh)

0 commit comments

Comments
 (0)