Skip to content

Commit 67af827

Browse files
committed
refactor(dstack-ingress): name the delegation zone after what it now is
ACME_CHALLENGE_ALIAS was accurate when the challenge was the only thing delegated. It now decides the target of four records -- the gateway pointer, the app-address TXT, the challenge TXT and the CAA -- so it is the delegation zone, and DELEGATION_ZONE is what it is called. It also lines up with DELEGATION_GATEWAY_RECORD. The old name still works. Each program that can be invoked on its own resolves both, rather than relying on a caller to normalise: certbot runs the challenge hook as a fresh process, and dnsguide.py and certman.py are runnable standalone. Also document that a wildcard and its base name share a delegated target -- both *.example.com and example.com map to example.com.<zone>. One deployment serving both publishes the same values twice and is fine; two deployments with different gateways need separate zones.
1 parent ca39932 commit 67af827

6 files changed

Lines changed: 34 additions & 18 deletions

File tree

custom-domain/dstack-ingress/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ environment:
191191
| `EVIDENCE_SERVER` | `true` | Serve evidence files at `/evidences/` on the TLS port |
192192
| `EVIDENCE_PORT` | `80` | Internal port for evidence HTTP server |
193193
| `ALPN` | | TLS ALPN protocols (e.g. `h2,http/1.1`). Only set if backends support h2c |
194-
| `ACME_CHALLENGE_ALIAS` | | Delegate the ACME DNS-01 challenge to this zone (see below) so the DNS token needs no access to the served domain's own zone |
194+
| `DELEGATION_ZONE` | | Zone this container writes into, so the DNS token needs no access to the served domain's own zone (see below). `ACME_CHALLENGE_ALIAS` is the original name and still works |
195195
| `ACME_CHALLENGE_PROPAGATION_SECONDS` | `120` | Wait after writing the delegated challenge TXT before validation. Must outlast the record TTL (60s), or a resolver still serving the previous attempt's value fails validation. Keep well under ~250s — certbot is killed after a 300s per-run timeout |
196196
| `DELEGATION_GATEWAY_RECORD` | `cname` | How the delegated name points at the gateway: `cname` (follows a gateway that moves) or `a` (for providers that refuse a CAA beside a CNAME) |
197197
| `ALLOW_MISSING_CAA` | `false` | In delegation mode, treat an unconfirmed `accounturi` CAA as a warning instead of a blocker. Default fails closed (see below) |
@@ -206,7 +206,7 @@ served name lives under a shared production zone (e.g. `svc.example.com` under
206206
`example.com`), that token can edit every record in the zone, which may be more
207207
privilege than you want.
208208

209-
Set `ACME_CHALLENGE_ALIAS=<delegation-zone>` to move every name this deployment
209+
Set `DELEGATION_ZONE=<delegation-zone>` to move every name this deployment
210210
needs into a zone your token controls. You create three CNAMEs in the served
211211
domain's zone **once, before deploying**, and then never touch DNS again — not
212212
when the app id changes with the compose, not when the ACME account is
@@ -240,6 +240,11 @@ itself and publishes an address record, which can carry the CAA. The cost is
240240
that a gateway which moves is then only picked up on the next pass, where a
241241
CNAME follows it immediately.
242242

243+
A wildcard and its base name share a delegated target: both `*.example.com` and
244+
`example.com` map to `example.com.<delegation-zone>`. One deployment serving both
245+
is fine, since it publishes the same values twice. Two deployments pointing at
246+
different gateways are not — give them separate delegation zones.
247+
243248
**Wildcards keep an operator-managed CAA.** RFC 8659 evaluates `*.example.com`
244249
at `example.com`, which is your own name and cannot be aliased away, so for a
245250
wildcard the container prints the CAA and verifies it rather than publishing it.

custom-domain/dstack-ingress/TESTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,13 @@ first issuance.
201201

202202
### Challenge delegation (dns-01)
203203

204-
`ACME_CHALLENGE_ALIAS` answers the DNS-01 challenge in a zone the container's
204+
`DELEGATION_ZONE` answers the DNS-01 challenge in a zone the container's
205205
token controls, so the token needs no access to the served domain's own zone.
206206
Testing it looks like it needs a second registrar account. It does not.
207207

208208
Any name under a zone you already control works as the delegation zone, because
209209
the provider resolves a zone by longest-suffix match on the record name. With
210-
`ACME_CHALLENGE_ALIAS=deleg.example.com` and a served domain of
210+
`DELEGATION_ZONE=deleg.example.com` and a served domain of
211211
`svc.example.com`, the challenge record is
212212
`_acme-challenge.svc.example.com.deleg.example.com`, which lands in
213213
`example.com` — the same zone, but through the delegation code path.

custom-domain/dstack-ingress/scripts/acme-dns-alias-hook.sh

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
#
44
# Instead of writing the `_acme-challenge` TXT into the served domain's own
55
# zone (which requires a DNS token for that zone), this writes it into a
6-
# delegated zone that our token controls (ACME_CHALLENGE_ALIAS). The served
6+
# delegated zone that our token controls (DELEGATION_ZONE). The served
77
# domain's zone only needs one static, operator-managed CNAME:
88
#
9-
# _acme-challenge.<domain> CNAME _acme-challenge.<domain>.<ACME_CHALLENGE_ALIAS>
9+
# _acme-challenge.<domain> CNAME _acme-challenge.<domain>.<DELEGATION_ZONE>
1010
#
1111
# Let's Encrypt follows that CNAME during validation and reads the TXT from the
1212
# delegated zone, so the enclave's token never needs access to the served
@@ -18,8 +18,12 @@ set -euo pipefail
1818

1919
action="${1:-}"
2020

21-
if [ -z "${ACME_CHALLENGE_ALIAS:-}" ]; then
22-
echo "acme-dns-alias-hook: ACME_CHALLENGE_ALIAS is not set" >&2
21+
# DELEGATION_ZONE is the current name; ACME_CHALLENGE_ALIAS is the original one
22+
# and still works. certbot runs this as a fresh process, so resolve both here
23+
# rather than relying on the caller.
24+
zone="${DELEGATION_ZONE:-${ACME_CHALLENGE_ALIAS:-}}"
25+
if [ -z "$zone" ]; then
26+
echo "acme-dns-alias-hook: DELEGATION_ZONE is not set" >&2
2327
exit 1
2428
fi
2529
if [ -z "${CERTBOT_DOMAIN:-}" ]; then
@@ -28,7 +32,7 @@ if [ -z "${CERTBOT_DOMAIN:-}" ]; then
2832
fi
2933

3034
# certbot always passes the base domain (no leading "*.") in CERTBOT_DOMAIN.
31-
record="_acme-challenge.${CERTBOT_DOMAIN}.${ACME_CHALLENGE_ALIAS}"
35+
record="_acme-challenge.${CERTBOT_DOMAIN}.${zone}"
3236

3337
case "$action" in
3438
auth)

custom-domain/dstack-ingress/scripts/certman.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,11 @@ def _build_certbot_command(self, action: str, domain: str, email: str) -> List[s
279279
"""Build certbot command using provider configuration."""
280280
certbot_cmd = self._get_certbot_command()
281281

282-
# Challenge-delegation mode: when ACME_CHALLENGE_ALIAS is set, answer the
282+
# Challenge-delegation mode: when DELEGATION_ZONE is set, answer the
283283
# DNS-01 challenge in a delegated zone via a manual hook instead of the
284284
# provider's certbot plugin, so our DNS token never needs access to the
285285
# served domain's own zone. See scripts/acme-dns-alias-hook.sh.
286-
if os.environ.get("ACME_CHALLENGE_ALIAS", "").strip():
286+
if os.environ.get("DELEGATION_ZONE", "").strip() or os.environ.get("ACME_CHALLENGE_ALIAS", "").strip():
287287
hook = "/scripts/acme-dns-alias-hook.sh"
288288
base_cmd = certbot_cmd + [action, "--non-interactive", "-v"]
289289
if action == "certonly":

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ source /scripts/functions.sh
1111
source /scripts/haproxy-lib.sh
1212
source /scripts/evidence-lib.sh
1313

14+
# The zone this container writes into. ACME_CHALLENGE_ALIAS was the original
15+
# name, from when the challenge was the only thing delegated; it now decides the
16+
# target of every record, so DELEGATION_ZONE is what it is called. The old name
17+
# still works.
18+
DELEGATION_ZONE=${DELEGATION_ZONE:-${DELEGATION_ZONE:-}}
19+
export DELEGATION_ZONE
20+
1421
# ACME_EMAIL / ACME_STAGING are normalised by entrypoint.sh and exported. The
1522
# contact address is optional; certman.py asks for a contactless account when
1623
# none is given.
@@ -121,7 +128,7 @@ set_txt_record() {
121128
# reject the CAA here; see DELEGATION_GATEWAY_RECORD.
122129
delegated_name() {
123130
local domain="${1#\*.}"
124-
echo "${domain}.${ACME_CHALLENGE_ALIAS}"
131+
echo "${domain}.${DELEGATION_ZONE}"
125132
}
126133

127134
delegation_publish() {
@@ -151,7 +158,7 @@ delegation_publish() {
151158
;;
152159
esac
153160

154-
txt_name="$(txt_record_name "$domain").${ACME_CHALLENGE_ALIAS}"
161+
txt_name="$(txt_record_name "$domain").${DELEGATION_ZONE}"
155162
dnsman.py set_txt --domain "$txt_name" --content "$(txt_record_value)" || return 1
156163
}
157164

@@ -195,7 +202,7 @@ delegation_guide() {
195202
--caa-name "$caa_domain" \
196203
--caa-tag "$caa_tag" \
197204
--challenge dns-01 \
198-
--challenge-alias "$ACME_CHALLENGE_ALIAS" \
205+
--challenge-alias "$DELEGATION_ZONE" \
199206
--mode "${DNS_SETUP_MODE:-wait}" \
200207
--include "$include" \
201208
"$@"
@@ -231,7 +238,7 @@ set_caa_record() {
231238
# Delegation is deliberately NOT gated by SET_CAA: this record is the only
232239
# thing stopping anyone else who can satisfy the delegated challenge, so it
233240
# is not optional here.
234-
if [ -n "${ACME_CHALLENGE_ALIAS:-}" ]; then
241+
if [ -n "${DELEGATION_ZONE:-}" ]; then
235242
delegation_publish_caa "$domain"
236243
case $? in
237244
0) return ;;
@@ -280,7 +287,7 @@ set_caa_record() {
280287
process_domain() {
281288
local domain="$1" first status
282289

283-
if [ -n "${ACME_CHALLENGE_ALIAS:-}" ]; then
290+
if [ -n "${DELEGATION_ZONE:-}" ]; then
284291
# Delegation: the operator created three CNAMEs once, before deploying.
285292
# Everything they point at is published here. Which of them block
286293
# depends on the pass.
@@ -297,7 +304,7 @@ process_domain() {
297304
# Publish first: the aliases the operator created point at names that
298305
# have to exist before checking them proves anything.
299306
if ! delegation_publish "$domain"; then
300-
echo "Error: could not publish $domain into $ACME_CHALLENGE_ALIAS" >&2
307+
echo "Error: could not publish $domain into $DELEGATION_ZONE" >&2
301308
return 1
302309
fi
303310
local want=challenge-cname

custom-domain/dstack-ingress/scripts/dnsguide.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ def main() -> int:
470470
parser.add_argument("--challenge", default="tls-alpn-01")
471471
parser.add_argument(
472472
"--challenge-alias",
473-
default=os.environ.get("ACME_CHALLENGE_ALIAS", ""),
473+
default=os.environ.get("DELEGATION_ZONE", os.environ.get("ACME_CHALLENGE_ALIAS", "")),
474474
help="delegation zone for the _acme-challenge CNAME (dns-01 delegation)",
475475
)
476476
parser.add_argument(

0 commit comments

Comments
 (0)