Skip to content

Commit 098741c

Browse files
committed
fix(dstack-ingress): reject tls-alpn-01 wildcards at startup, not per pass
A wildcard under tls-alpn-01 can never succeed -- RFC 8737 forbids the challenge for wildcard identifiers -- so it is a configuration error, not a runtime failure. process_domain did reject it, but only once the certificate loop reached that domain, by which point the container had generated a placeholder certificate for a name that will never get a real one, started haproxy serving it, and settled into retrying an unsatisfiable config every 60s to 1800s forever. Check it once at the top of tlsalpn.sh, before the startup sequence that does all of the above. It belongs to tls-alpn-01, not to entrypoint.sh: that file validates what both modes share and dispatches, and putting a CHALLENGE_TYPE branch back in it is the mode-specific logic the split was meant to remove -- HTTP-01 would then add a third. Deliberately fatal for the container rather than skipping the one domain. A mixed config did already contain the damage -- the other domains were issued normally -- but it left a self-signed certificate served for the wildcard indefinitely and a permanently failing loop, which reads as a TLS bug rather than a typo. The per-domain check is gone with it: the domain list comes from env vars fixed for the container's lifetime, so it saw exactly what the startup check saw and could not fire. legoman.py keeps its own check, which is a real boundary -- it guards the ACME call and can be invoked standalone. dns-01 wildcards are unaffected.
1 parent 4d54843 commit 098741c

2 files changed

Lines changed: 28 additions & 6 deletions

File tree

custom-domain/dstack-ingress/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,10 @@ Encrypt dropping expiry mail, and the container passes it for you.
325325
### Limitations
326326

327327
- **No wildcards.** RFC 8737 forbids tls-alpn-01 for wildcard identifiers, and
328-
the CA will not offer the challenge. Use `dns-01` for `*.example.com`.
328+
the CA will not offer the challenge. The container refuses to start rather
329+
than serve a name it can never obtain a certificate for, so a wildcard
330+
anywhere in `DOMAIN`/`DOMAINS` is a startup error even if the other names are
331+
fine. Use `dns-01` for `*.example.com`.
329332
- **The gateway must be reachable on port 443.** The CA connects to port 443 of
330333
whatever the CNAME resolves to; the port is fixed by the protocol.
331334
- **CAA must permit `tls-alpn-01`.** A record left over from a `dns-01`

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,26 @@ require_instance_id() {
8989
fi
9090
}
9191

92+
# RFC 8737 forbids tls-alpn-01 for wildcard identifiers, so a wildcard here can
93+
# never succeed. That makes it a configuration error, and it has to be caught
94+
# before the startup sequence below: past that point the container has generated
95+
# a placeholder certificate for a name that will never get a real one, started
96+
# haproxy serving it, and settled into retrying an unsatisfiable config forever.
97+
#
98+
# Fatal for the container rather than skipping the one domain. A mixed config
99+
# does contain the damage -- the other domains are issued normally -- but it
100+
# leaves a self-signed certificate served for the wildcard indefinitely and a
101+
# permanently failing loop, which reads as a TLS bug rather than a typo.
102+
reject_wildcards() {
103+
local wildcards
104+
wildcards=$(get-all-domains.sh | grep '^\*\.' || true)
105+
[ -n "$wildcards" ] || return 0
106+
echo "Error: tls-alpn-01 cannot issue wildcard certificates (RFC 8737)." >&2
107+
echo "$wildcards" | sed 's/^/ /' >&2
108+
echo "Use CHALLENGE_TYPE=dns-01 for wildcards, or list the names individually." >&2
109+
exit 1
110+
}
111+
92112
# What the gateway should route this hostname to.
93113
#
94114
# dns-01 uses the app id, so the gateway load-balances across every instance of
@@ -190,11 +210,9 @@ collect_evidence() {
190210
process_domain() {
191211
local domain="$1"
192212

193-
if [[ "$domain" == \*.* ]]; then
194-
echo "Error: cannot issue a wildcard certificate for $domain with tls-alpn-01." >&2
195-
echo "RFC 8737 forbids it; use CHALLENGE_TYPE=dns-01 for wildcards." >&2
196-
exit 1
197-
fi
213+
# No wildcard check here: reject_wildcards has already refused to start over
214+
# the same domain list, which cannot change while the container runs.
215+
# legoman.py checks independently, guarding the ACME call itself.
198216

199217
# Register the ACME account first so the CAA record we print can already
200218
# pin accounturi. Without this the operator would have to add CAA in a
@@ -303,6 +321,7 @@ cert_loop() {
303321
done
304322
}
305323

324+
reject_wildcards
306325
setup_py_env
307326
check_lego
308327
load_dstack_identity

0 commit comments

Comments
 (0)