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

RecordWritten byMechanismDeletion gate
the gateway hostname (gateway.host, optional)factory host upa 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 / syncidempotent 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 nameResult
nothingcreated, tagged
our record, already pointing at the tunnel, proxiedunchanged (no API write)
our record, stale content or unproxiedupdated 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 toolrefused — reported as a warning, left untouched
a non-CNAME record (A, TXT, …), tagged or notrefused — 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-alice resolves on the tailnet; the device name is the container’s, matched exactly on removal (container/tailscale.ts), and is not a zone record.