Skip to content

feat(dstack-ingress): ACME DNS-01 challenge delegation (CNAME alias) for least-privilege DNS tokens#104

Open
Ravenyjh wants to merge 2 commits into
Dstack-TEE:mainfrom
Ravenyjh:feat/acme-challenge-delegation
Open

feat(dstack-ingress): ACME DNS-01 challenge delegation (CNAME alias) for least-privilege DNS tokens#104
Ravenyjh wants to merge 2 commits into
Dstack-TEE:mainfrom
Ravenyjh:feat/acme-challenge-delegation

Conversation

@Ravenyjh

Copy link
Copy Markdown

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-challenge TXT, plus the A/alias record and — with SET_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 — when ACME_CHALLENGE_ALIAS is set, run certbot via --manual with auth/cleanup hooks instead of the provider's DNS plugin. renew reuses the authenticator + hooks saved in the renewal config.
  • scripts/acme-dns-alias-hook.sh (new) — writes/removes the _acme-challenge TXT in the delegation zone, reusing dnsman.py / the existing dns_providers abstraction.
  • dnsman.py + dns_providers/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 it prints the exact records the operator must set statically. For CAA it does not silently skip: it prints the required accounturi CAA 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 accounturi CAA (only this enclave's ACME account may issue) normally auto-set by entrypoint.sh cannot 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_ALIAS unset, 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

Ravenyjh and others added 2 commits July 27, 2026 19:13
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant