tenant-iso-domains
Fixes the three findings in #215 (tenant-isolation audit, 2026-10-03). The branch is rebased onto current main, after first-route-wildcard, wildcard-zone-check, instance-catchall and #209 landed. Paul approved the design (TXT proof behind CONSOLEDOMAINPROOF, off by default) and the copy verbatim. The Fable review approved 16786404 with fixes; those fixes are in this tip and listed at the end.
What changes
1. Proof of control for custom hostnames, opt-in per deployment (CONSOLE_DOMAIN_PROOF).
With the setting off (the default), nothing changes: attach is first-writer-wins, every claim is written verified, and the console makes no TXT lookups. AGENTS.md and the README document the setting.
With the setting on:
- A member's attach is stored pending unless a TXT record at
_carlos-challenge.<host>carries the account's token. The token is derived from the deployment, the account and the host, and is not secret. - A pending claim derives nothing in
ReconcileAliases, so it is never routed or certified. It also holds no parent for an instance catch-all. - A first-route attach declares no provision record until the claim is proven.
- Repeating the attach verifies the claim, and so does
carlos domains verify. A plain re-attach of a pending first-route claim also looks for the proof. - A pending claim yields to a proven attach at any time.
- A reservation is never renewed.
pending_untilis fixed at first attach + 7 days, so a squatter must prove control within 7 days of first attaching. After that, anyone can take the name. - No record is needed for: - the account's own
<sqid>.<apps-domain>; - names delegated throughCONSOLE_APPS_DOMAIN_ALIAS_ACCOUNTS; - #209 platform names.
The wire changes are additive. Claims without a pending key read as verified.
2. An account's apps-domain namespace is its own (always on).
- Another account's
<sqid>.<apps-domain>, whether that account is live or deleted (tombstoned sqids are reserved too), ishostReservedindomainHostPolicy. - A first route may not be minted under a sqid-shaped namespace that is not the caller's own.
edge.parentsForderives a wildcard parent only from sqid-shaped labels.
3. The fleet store door (always on).
- A box reads only its own account's verified claim bodies, with
created_bystripped. Every other claim body gets the door's byte-identical 404. - The listing still names every claimed hostname. The alias pass decides "someone else claimed this name" from the listing (for instance catch-alls), so a narrowed listing let a box shadow another tenant's claim.
- So a fleet box can still enumerate claimed hostnames. It cannot see who holds them, which app they belong to, their paths, or whether they are proven.
- Old boxes still work: they get the same JSON shape, minus one optional field.
Roll order and rollback
- With proof off, there is no ordering constraint.
- Before setting
CONSOLE_DOMAIN_PROOF=true, roll the non-fleet boxes first. A pre-#215 box reads the bucket directly and would route a pending claim. Fleet boxes are covered by the door. - Don't roll the console back past this commit while
CONSOLE_DOMAIN_PROOFis on. A pre-#215 console's attach converge drops the unknownpendingfield, so a re-attach would rewrite a pending claim as verified.
Accepted risks
- A pending claim counts as a hold in
apex.gohostClaimHolder. An app named after a pending custom hostname is refused, as it would be for a verified claim. - An account can detach a lapsed reservation and attach it again to start a fresh 7 days. Re-attaching alone never extends a reservation, and anyone who proves control supersedes it at any time.
- The fleet door listing reveals which hostnames are claimed (see item 3).
How it was tested
New tests:
internal/domains/claims_proof_test.go: covers pending-parent catch-alls and the rule that a reservation is not renewed.internal/console/domainproof_test.go: with proof off, behaviour matches main; with proof on, it covers takeover, replay, a squatter, first-route claims, plain re-attach of a pending first-route claim, the namespace rules, and deleted-account namespaces.TestStoreDoorClaimsAreOwnAccountAndRedacted.TestFleetBoxCatchallYieldsToAnotherTenantsClaim: a real fleet box through the door. An unclaimed name derives an alias; another tenant's claimed name does not.parentsForcases.
Gate, run with GOFLAGS=-buildvcs=false and a short TMPDIR:
gofmt,buildandvetare clean.- The domains, console, docsite and cmd/console tests pass.
- Root
TestE2EDomainsAttachThroughDeploypasses. make testfails only on environment problems: socket paths too long ("bind: invalid argument") in activator, edge ListenPurge, instanceexec and host ScheduledTickWakes; and the root package reading~/.carlos/config.
Review fixes in this tip
- The fleet door listing names every claim again, which fixes the catch-all shadowing regression. A test covers it.
- A plain re-attach of an own pending first-route claim now goes through the proof path; before, it short-circuited and reported verified.
- Reservations are capped at first attach + 7 days.
- The rollback note above.
- Deleted accounts' namespaces are reserved.
Not done
- No verify button on the Settings page.
- Verification is one-shot.
Discussions 0 open
- No discussions on this branch yet.
Tasks
- No tasks yet.
Activity
- claude agent, operated by paul@keymail.dev replied to Custom domain claims need proof of control: dangling-DNS takeover, squatting, unreserved account namespaces, claims readable fleet-wide
- claude agent, operated by paul@keymail.dev opened Custom domain claims need proof of control: dangling-DNS takeover, squatting, unreserved account namespaces, claims readable fleet-wide