deploy: auto-reconcile public DNS CNAMEs via Cloudflare

castle deploy now manages the public zone's tunnel CNAMEs to match the current
set of public: true services — creating missing records and deleting removed
ones — instead of only printing manual 'cloudflared tunnel route dns' hints.

New core/generators/dns.py reconciles via the Cloudflare API (stdlib only). It
only ever touches records whose content is <tunnel_id>.cfargotunnel.com, so
hand-managed records in the same zone are never altered. deploy._write_tunnel_config
calls it in both branches (delete of the last public service cleans up its CNAME).
Without a token it's a no-op and the manual route-once hints are surfaced as before.

Token: ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN, a single DNS:Edit permission
scoped to the public zone (the 'Edit zone DNS' template) — confirmed sufficient:
that one permission resolves the zone by name and edits records, no separate
Zone:Read needed. Separate from CLOUDFLARE_API_TOKEN (ACME, internal zone).

castle doctor gains a read-only probe that WARNs when the token is absent (CNAMEs
stay manual) and FAILs when it can't reach the zone/records.
This commit is contained in:
2026-07-02 11:02:34 -07:00
parent 2adc073863
commit 9028d15ec6
4 changed files with 229 additions and 12 deletions

View File

@@ -89,12 +89,10 @@ proxy: true # required — the service must be routed
public: true # also expose at <name>.pub.payne.io via the tunnel
```
Then deploy and route DNS (a CNAME `<name>.pub.payne.io → <tunnel>.cfargotunnel.com`,
created once per public host — `castle deploy` prints the exact command):
Then just deploy:
```bash
castle deploy
cloudflared tunnel route dns <tunnel-id> <name>.pub.payne.io
```
`castle deploy` regenerates `~/.castle/artifacts/specs/cloudflared.yml` from the
@@ -102,6 +100,35 @@ current set of public services and restarts `castle-tunnel`. Flip `public` back
`false` (or remove it) and redeploy to un-expose — the hostname drops out of the
ingress immediately.
### DNS: automatic (with a token) or manual
Each public host needs a CNAME `<name>.pub.payne.io → <tunnel>.cfargotunnel.com`.
Castle can manage these for you. Create a Cloudflare API token with a single
**`DNS:Edit`** permission scoped to the **public zone** — this is exactly
Cloudflare's built-in *"Edit zone DNS"* template. That one permission is
sufficient: it resolves the zone by name *and* creates/deletes records (no
separate `Zone:Read` is needed — verified against `domain0.org`). An account-owned
token (`cfat_…`) works (that's what this was confirmed with). Drop it into the
secret store:
```bash
printf %s "<token>" > ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN
chmod 600 ~/.castle/secrets/CLOUDFLARE_PUBLIC_DNS_TOKEN
```
With it present, every `castle deploy` **reconciles** the public CNAMEs against the
current public set — creating missing ones and deleting removed ones — and prints a
one-line summary. It only ever touches records pointing at *this* tunnel
(`<tunnel_id>.cfargotunnel.com`), so hand-managed records in the same zone are safe.
This token is separate from `CLOUDFLARE_API_TOKEN` (which is the ACME token for the
internal zone); the public zone is usually a different zone/account.
Without the token, `castle deploy` instead prints the exact command to run per host:
```bash
cloudflared tunnel route dns <tunnel-id> <name>.pub.payne.io
```
## The part that isn't the tunnel
Reachability is the easy half. Anything public also needs, per service: