feat(dstack-ingress): ACME DNS-01 challenge delegation (CNAME alias) for least-privilege DNS tokens#104
Open
Ravenyjh wants to merge 2 commits into
Open
Conversation
Adds an opt-in `ACME_CHALLENGE_ALIAS` mode so the DNS token can be scoped to a delegated zone instead of the served domain's own (often shared, production) zone. Closes the least-privilege gap described in the issue. - certman.py: when ACME_CHALLENGE_ALIAS is set, run certbot via `--manual` with auth/cleanup hooks instead of the provider's DNS plugin; `renew` reuses the hooks saved in the renewal config. - acme-dns-alias-hook.sh: writes/removes the `_acme-challenge` TXT in the delegated zone (reusing dnsman.py / the existing provider abstraction). - dnsman.py + base.py: add `unset_txt` for challenge cleanup. - entrypoint.sh: in delegation mode the served-zone records (alias, app-address TXT, CAA) can't be written by our token, so print the exact records the operator must set statically, and for CAA verify presence (via DoH) and warn loudly if missing — instead of silently dropping the accounturi lock. - README: document the mode, required static records, and the CAA security note. Non-breaking: unset ACME_CHALLENGE_ALIAS = current behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xa1zFJs3FVP8UTsSWuS3Yj
…ldcard CNAME, robust DoH verify - entrypoint.sh: the delegation-mode accounturi-CAA check was unreachable in the default config (it lived after the `SET_CAA != true` early return, and SET_CAA defaults to false), so the forge-prevention control silently never ran. Move it above the SET_CAA gate (delegation CAA is independent of SET_CAA) and make it fail closed: if the required CAA is confirmed absent the container exits, with ALLOW_MISSING_CAA=true to override. A transient DoH failure only warns. - entrypoint.sh: fix the wildcard `_acme-challenge` CNAME guidance — strip `*.` so it matches the base name certbot/LE validate and the hook writes to. - entrypoint.sh: parse the DoH response with jq and match with `grep -F` (the account URI contains `/` and `.`, which must not be regex), and distinguish "confirmed absent" (fail) from "could not verify" (warn). - base.py: unset_txt_record no longer reports success for a record it could not address (no id) — counts it as failure and warns. - README: document fail-closed CAA + ALLOW_MISSING_CAA, the propagation ceiling vs certbot's 300s timeout, using a dedicated delegation zone, and deleting a stale plugin renewal conf before switching to delegation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xa1zFJs3FVP8UTsSWuS3Yj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements #103.
Problem
dstack-ingress issues certs via ACME DNS-01, which needs a DNS token that can edit the served domain's own zone (
_acme-challengeTXT, plus the A/alias record and — withSET_CAA— the CAA). When the served name lives under a shared production zone, that token can edit the whole zone, which is more privilege than an enclave should hold. Cloudflare tokens can't scope below zone level, and subdomain zones are Enterprise-only.What this adds
Opt-in
ACME_CHALLENGE_ALIAS=<delegation-zone>: answer the DNS-01 challenge in a separate zone the token controls, so the token never touches the served domain's zone.certman.py— whenACME_CHALLENGE_ALIASis set, run certbot via--manualwith auth/cleanup hooks instead of the provider's DNS plugin.renewreuses the authenticator + hooks saved in the renewal config.scripts/acme-dns-alias-hook.sh(new) — writes/removes the_acme-challengeTXT in the delegation zone, reusingdnsman.py/ the existingdns_providersabstraction.dnsman.py+dns_providers/base.py— addunset_txtfor challenge cleanup.entrypoint.sh— in delegation mode the served-zone records (alias, app-address TXT, CAA) can't be written by our token, so it prints the exact records the operator must set statically. For CAA it does not silently skip: it prints the requiredaccounturiCAA and verifies its presence (via DoH), warning loudly if absent — since that CAA is the forge-prevention.README.md— documents the mode, the required static records, and the security note.Security note
The
accounturiCAA (only this enclave's ACME account may issue) normally auto-set byentrypoint.shcannot be set by us in delegation mode (no token for the served zone). Rather than silently dropping it (which would let anyone who satisfies the delegated challenge obtain a cert for the domain), the feature prints the exact record and verifies/warns. The delegation token should be scoped only to<delegation-zone>.Compatibility
Non-breaking: with
ACME_CHALLENGE_ALIASunset, behavior is unchanged.Testing
Python (
py_compile) and bash (bash -n) syntax checks pass. This is the DNS/orchestration path; happy to add an e2e case (Let's Encrypt staging + a delegated test zone) if you'd like.🤖 Generated with Claude Code
https://claude.ai/code/session_01Xa1zFJs3FVP8UTsSWuS3Yj