DNS ownership
Who may write which record in a zone the factory touches, and how the engine
tells its own records from everyone else’s. For host owners; the manifest side
is docs/factory-yml.md §hostnames, the host side docs/hosts.md §dns.
The tag
Every record the factory writes carries an ownership tag in the record’s Cloudflare comment:
managed-by=factory/<host id>
<host id> is the id of the host spec (hosts/<name>.yml). The tag is the
ONLY thing the engine consults before it updates or deletes a record. Name,
type and content are never enough: a record that merely shares a name with one
of ours is somebody’s record, and the legacy provisioning (which matched by
name alone) would have overwritten it.
There is one tag per host, not per container or per user. A host’s containers are provisioned by many actors with one credential; the unit of ownership is the thing that holds the credential.
Two kinds of record
| Record | Written by | Mechanism | Deletion gate |
|---|---|---|---|
the gateway hostname (gateway.host, optional) | factory host up | a Pulumi resource, { protect: true } | the preview gate (host/dns-gate.ts) fails closed on any DNS delete |
container hostnames (dev-alice.<zone>, marketplace-dev-alice.<zone>, …) | factory container provision / sync | idempotent API writes (container/dns.ts) | in code: only a record carrying THIS host’s tag is ever PUT or DELETEd |
Container records are deliberately not Pulumi resources: N developers
provisioning in parallel would serialise on the one host stack, and each
provision would be a pulumi up of the entire host. The price is that the
preview gate never sees them — which is why the ownership check lives in the
code path itself, asserted before every write, and is pinned by
packages/engine/test/container/dns.test.ts.
What a provision does, per public hostname
The set is hostnames.default plus one hostnames.port rendering per
public: true port that is neither default nor path-routed. A private port
has no hostname and no record. Each is a proxied CNAME to the host’s
tunnel (<tunnel id>.cfargotunnel.com), TTL auto.
| The zone holds under that name | Result |
|---|---|
| nothing | created, tagged |
| our record, already pointing at the tunnel, proxied | unchanged (no API write) |
| our record, stale content or unproxied | updated in place |
an UNTAGGED CNAME (no managed-by= in the comment — a legacy dev-<user> record, or one made by hand) | adopted: updated in place, the tag added. Never duplicated beside it. |
a record tagged managed-by= by another host or tool | refused — reported as a warning, left untouched |
| a non-CNAME record (A, TXT, …), tagged or not | refused — it is someone’s real record |
Adoption is what lets a repo move from hand-managed records to the factory
without a DNS outage: the first provision claims the existing name and from
then on the record is the factory’s. It is narrow on purpose (CNAME only,
never over another owner’s tag) so that a typo in hostnames.default cannot
capture an unrelated record.
What a teardown / purge does
Deletes every record under the container’s hostname set that carries this host’s tag. Anything else under those names is reported and left alone — including an untagged legacy record, because teardown never adopts. A refusal is a warning, not a failure: the container is down either way, and a foreign record is the zone owner’s to sort out.
What the credential can do
dns.tokenFrom (default secret:cloudflareApiKey) needs Zone:DNS:Edit on
every zone in dns.zones[] and nothing more. The engine never lists a zone’s
records wholesale — every lookup is by exact name — so a token scoped to the
listed zones is the whole blast radius. A record in a zone outside
dns.zones[] is refused at factory validate before any credential is used.
Recognising a factory record in the dashboard
Filter the zone’s records on the comment managed-by=factory/. The value
after the slash names the host; two hosts writing into one zone (a staging and
a production factory, say) are told apart by it, and neither will touch the
other’s records.
Deliberately not here
- No wildcard.
*.<zone>→ tunnel would make every hostname resolve, including ones no container serves (a 404 from Traefik that looks like a dead environment). One record per served hostname, created and removed with the container, is the observable state. - No record for private ports. Reachable inside the container and over the tailnet only.
- No Tailscale MagicDNS records.
ssh developer@dev-aliceresolves on the tailnet; the device name is the container’s, matched exactly on removal (container/tailscale.ts), and is not a zone record.