carlos / platform Public

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_until is 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 through CONSOLE_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), is hostReserved in domainHostPolicy.
  • A first route may not be minted under a sqid-shaped namespace that is not the caller's own.
  • edge.parentsFor derives 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_by stripped. 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_PROOF is on. A pre-#215 console's attach converge drops the unknown pending field, so a re-attach would rewrite a pending claim as verified.

Accepted risks

  • A pending claim counts as a hold in apex.go hostClaimHolder. 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.
  • parentsFor cases.

Gate, run with GOFLAGS=-buildvcs=false and a short TMPDIR:

  • gofmt, build and vet are clean.
  • The domains, console, docsite and cmd/console tests pass.
  • Root TestE2EDomainsAttachThroughDeploy passes.
  • make test fails 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

  1. The fleet door listing names every claim again, which fixes the catch-all shadowing regression. A test covers it.
  2. A plain re-attach of an own pending first-route claim now goes through the proof path; before, it short-circuited and reported verified.
  3. Reservations are capped at first attach + 7 days.
  4. The rollback note above.
  5. Deleted accounts' namespaces are reserved.

Not done

  • No verify button on the Settings page.
  • Verification is one-shot.
Commits 0
Discussions 1 0 open
Tasks 0/0 done

Discussions 0 open

  • No discussions on this branch yet.

Tasks

  • No tasks yet.

Activity