carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

Instance catch-alls: <label>.<parent> serves the app's instance <label>

Date: 2026-10-05 Status: branch instance-catchall on amadan, not landed. Revised after the review on #233 (§8).

1. Problem

Jelly (lazyatom/jam) runs one instance of dev-managed-jelly per team, at <team>.bad.carlos.exciting.io. Teams should also answer at <team>.letsjelly.dev. Today the manager makes that happen with one exact claim per team at signup (AttachDomain in manager/carlos.go), plus a wait for DNS. The existing catch-all (Wildcard+Catchall on a claim) cannot do it, because it sends every subdomain to ONE route (domains.ReconcileAliases, the *.<host> → primary row). It cannot fan out to per-team instances, and it belongs to whoever holds the apex claim. Here that is the static site dev-jelly-site.

2. Design

The claim. An instance catch-all is a domains.Claim at the key domains/*.<parent>.json, with Host: "*.<parent>", Account, App, Instances: true and nothing else: no Route, Wildcard, Catchall or Paths.

  • The star in the key means there is one catch-all per parent, held by the same create-only bucket-key gate that every claim uses.
  • No attach door accepts a * (NormalizeAliasHost), so a catch-all can never collide with an ordinary claim.

The door. carlos domains attach --app <a> '*.<parent>' (API: the existing attach body with host: "*.<parent>"). detach removes it the same way. Writing a catch-all requires all of these:

  1. A person (apiPerson), like carlos instances enable. A catch-all is standing policy over every unclaimed name under the parent. A credential the app holds must not be able to set it for itself. Without this, an app token could also put its instances under a sibling app's parent.
  2. No route, kind, channel, wildcard, catchall or platform field in the request. They are refused, not ignored.
  3. The app has an instance manifest. Without one it has nothing to serve. An app without one is also the only kind of app a rename can still move, which would leave the claim naming a dead app.
  4. The parent passes catchallParentRefused. It refuses a parent when: - the parent is in a deny tier (reserved or platform, so operators are refused too); - any console, reserved or platform hostname is equal to the parent, under it, or above it. The box cannot see the deny lists, so a derived name could otherwise land on the tunnel host or a console host; - the parent is equal to the apps domain, under it, or above it. That space holds the account-sqid and app-apex names. This is checked whatever CONSOLE_RESERVE_PLATFORM_DOMAINS says: on a self-hosted box the apps domain is not reserved, and edge.<apps-domain> would otherwise be derivable; - the parent is a public suffix (co.uk, github.io); - the parent is under the sidecar suffix.
  5. The parent's own claim is held by the same account, by any of its apps (domains.AttachInstanceCatchall). With wildcard DNS at the edge, a catch-all covers every unclaimed name under the parent at once, so only whoever already holds the parent may make one. If the parent has no claim at all, the request is refused: claim the parent first (an alias to any route of the account will do). Holding a catch-all in the same account but under a different app from the apex is the point of the design (static site at the apex, teams below it).

On the box. ReconcileAliases gains deriveInstanceAliases. For each catch-all whose parent is still claimed by the same account, it walks the box's local routes and derives one ordinary route_aliases row, <rt.Slug>.<parent> → rt.Host, for each route that meets every one of these:

  • Provisioned, not a sidecar, and not a replica;
  • the claim's own app, and the claim's own account (RouteAccount);
  • a Slug that is a single DNS label and is the first label of the host. The instance create door makes every record this shape.

A derived name loses to, in this order:

  1. any claim on that exact name, whoever holds it. This covers the manager's existing per-team claims, www, auth, and another account's claim made before the catch-all existed;
  2. a real route on this box;
  3. a real route on any live box (deploymentRoutes).

The desired set is rebuilt on every pass:

  • a deleted or moved instance drops its name on the next pass;
  • a detached catch-all drops every name;
  • a released exact claim hands its name back to the catch-all.

Rows are written only when they change, so a steady state causes no edge reload. At most 1000 derived names per box (maxInstanceAliases), in a deterministic order, with the overflow logged (§4.11).

What the derived rows are. They are the same rows an exact alias claim for each name produces today: Alias, Host, Account, App. That is the property the rest of this note relies on. Every reader of route_aliases already handles such rows correctly, and none of them looks for a claim behind a row. The only code that deletes alias rows is the pass's own desired-set sweep. The edge itself needed only one change, made after review: the cross-box precedence fix in §8, item 5.

The attach guard. While a catch-all is live, the attach doors refuse other accounts the one-label names under its parent that it reserves (domains.ForeignInstanceCatchall, then catchallInstanceName in the console). A refused attach gets the neutral 409 from the same place deploymentRouteTaken gives it, in all three attach paths (API alias, form alias, first-route). What it reserves (§8, review P1s):

  • Proved parent: every label. The account proves it controls the parent in DNS with TXT _carlos-catchall.<parent> = carlos-catchall-account=<sqid>. The value names the account, the same shape as the mail ownership proof (#216). - The guard checks the record live, so it follows the domain's DNS rather than a stamp taken once. - A lookup failure counts as "not proved": the narrower reservation, never a refusal. - carlos domains list reports reserves: all or reserves: instances on the catch-all's row, and names the record when it is missing.
  • Unproved parent: the labels of the app's existing instances, under any instance domain the app has used. These are counted from the app's instance record keys, so a manifest-domain change does not orphan them.
  • Why it exists. Without it, any account could attach acme.letsjelly.dev. Its exact claim would beat the derived name and take a live team's address. Attach needs no DNS proof, so nothing else stops that.
  • Why an unproved parent holds less (first review, finding 3). Attach needs no DNS proof, so a parent claim proves nothing. Holding every label for an unproved parent would let one squatted apex claim plus a catch-all block the real domain owner from every name under it.
  • One label only, because a catch-all serves one label only. Deeper names stay as open as they are today.
  • The caller's own existing claim is never refused (review finding 1). A claim made before the catch-all keeps re-attaching and converging. titogo's router re-attaches every name on every pass. deploymentRouteTaken makes the same allowance.
  • "Live" is the box's rule: the catch-all's parent is still claimed by its account. A catch-all whose parent was released, or has moved to another account, holds nothing. The parent's new holder may replace it (AttachInstanceCatchall overwrites a dead one).
  • Read failures. A bucket read failure in the guard is a retryable 502. It is never a 409 (which would read as "held") and never a skipped guard.

Precedence is decided from the listing (review finding 2). The box decides "a claim on this name exists" from the bucket listing's keys, not from the claim bodies that decoded this pass. A body whose GET fails is still a claim, so a spurious store error cannot hand another account's name to a derived alias for a pass.

Certificates. Each derived name gets per-host HTTP-01/ALPN certificates exactly as exact aliases do: the cert warm orders them once DNS points here, and the first handshake orders one otherwise. A delegated wildcard certificate is optional on top, by setting --wildcard on the parent's own claim. It then covers every derived name on every box (§4.10).

3. Mixed versions

  • An old box or old console reading a catch-all decodes a claim with no Route, no Wildcard and no Paths. Every pass already skips that shape. It logs nothing and makes no DNS lookup or ACME order for the literal *.<parent>. Certwarm drops *. hosts anyway.
  • An old console lists the row with an empty route and "checking" state. Its Detach button answers 404, because NormalizeAliasHost refuses the star. No old door can rewrite the claim, for the same reason, so the unknown instances field cannot be stripped.
  • Keys: * is safe in S3 keys, in DirStore on Linux, and through the fleet store door. canonicalKey accepts it, and aclRead and aclListScope already grant domains/** to fleet boxes. DirStore on Windows would refuse it, but no deployment bucket runs there.
  • A new console with old boxes: the catch-all is inert until the boxes upgrade. The attach guard is already live at that point, so other accounts are refused names the boxes do not serve yet. That is harmless, and it is what the owner asked for.

4. Deployment types

Each item says whether it is affected and what guards it.

  1. Flagship (console as an app, carloku.com, #209). Not affected unless a catch-all is made, and none can be made on the deployment's own zones: refused for everyone, operators included. The console's names, steering, tunnel and the apps domain are refused at, under and above the parent. There is no --platform form; it can be added if the flagship ever wants one.
  2. Tito / titogo. Its router re-attaches hostnames with an instance token, and it cannot create or remove a catch-all: those are person-only. Store-mode custom instance domains (go.tito.io) are unaffected, since nothing derives without a catch-all. The re-attach loop pays one extra GET per attach (*.<parent> of the host, usually a 404). A Hetzner spurious 403 there is a retryable 502. Re-attach is idempotent, so the next pass succeeds.
  3. Pages-hub one-route catch-all, and woodstar.app (apex plus Wildcard+Catchall, same app). Unaffected unless an instance catch-all is added on the same parent. If one is, derived exact names beat the route catch-all name by name (lookupIn: exact key first, then *.). That is same-account by construction, and it is the useful combination: unknown labels fall to the site, known ones to their team. - On woodstar it would take a PDS handle whose label matches an instance label, so do not combine the two there. - The ceecb3a9 self-removal loop cannot recur. Derived rows are exact names published with Alias: true, and DeploymentRoutes counts only Role==home && !Alias.
  4. Self-hosted single box, and this hand-staged box. Same as above: nothing changes without a catch-all. The apps domain is refused as a parent even though it is not reserved there. Hand-added routes (carlos route add) are not Provisioned and derive nothing. That also keeps a legacy account-name column, which reads as ops, out of reach.
  5. Customer fleets / carlos apps place. Fleet boxes read domains/** through the store door, so they derive for their own instances. The store door does not grant control/edges/, so deploymentRoutes is nil (permissive) there. This gap already exists for plain aliases, and the exact-claim skip still applies.
  6. Multi-box, multi-region edges, the edges file. Derived rows publish exactly like exact aliases: only once StampHealth has stamped them pointing, and backhauled by remote edges to the home box. A new or moved name can take up to about 12 minutes to reach other edges (one stamp period plus one edges-file period). Exact claims have the same delay today; a single box serves at once. Duplicate publication (everywhere-class instances) folds to the first-sorted box, as for exact aliases.
  7. Edge replicas, steering. - Replica rows are excluded explicitly (rt.Replica). A door-created replica carries the label as its Slug, so the Slug shape alone would not exclude it. - Steering looks at platform names only, and never at aliases or claims.
  8. Instance moves. During the cut the source row is moving, and the edge and edges file skip it. After the flip the target box derives the name. The name follows the instance, as an exact alias claim would.
  9. Hibernation and activation. An alias entry is its primary's upstream copied whole, route and hibernating included. A request on a derived name therefore wakes the instance itself, and metering, the gate and analytics are keyed on rt.Host. - The response cache keys on the requested host, so the app purges a derived name's entries with Carlos-Purge-Host. That is pre-existing for exact aliases. - The backend sees Host: <label>.<parent>, so the app must accept it. Jelly does today, via the exact claims.
  10. Isolated exec, path-prefix maps. - Isolated exec: units and sockets are keyed on rt.Host, so it is unaffected. - Path-prefix maps: a derived name inherits its instance's map, as any alias does. A Paths claim on a derived name itself is an exact claim and suppresses the derived row.
  11. Platform apps-domain wildcard, platform-domain claims (#209), reserved/deny lists, readings sweep, cert sweep, LE limits, alias-table growth, maxWildcardDomains. - parentsFor takes only platform/provisioned non-alias hosts, so derived names cannot mint platform parents. - The deny tiers are covered in §2. - The readings sweep (StampHealth) does one serial LookupHost per alias (3s timeout) every 5–6 minutes. Each alias adds about 140 bytes to the box's status snapshot, and readers skip a snapshot above 1 MiB. A skipped box also drops out of DeploymentRoutes and the edge-IP fold. That is why there is a per-box cap of 1000 derived names, logged when it is hit. - The cert sweep is expiry-only, and ownsCertName checks deploymentHost, not claims. - Let's Encrypt: without a wildcard, N instances means N new CT-logged certs against the registered domain's limit (50 per 7 days as of writing). About 200 teams take about four weeks of issuance; past the limit, certwarm backs off 12h per host. That is identical to today's per-team exact claims. The remedy is --wildcard on the parent claim, which needs CARLOS_ACME_DELEGATION_ZONE and a DNS provider. The testbed has neither. - The instance catch-all is not a Wildcard claim, so it takes no slot in maxWildcardDomains or maxDelegatedParents.
  12. Trash, purge, rename and move of apps; instance delete with parked data. - Trash drops the app's routes, so the derived names go with them. The catch-all claim stays, as every claim does, and comes back to life on restore. - Purge never deletes domains/ (pre-existing for all claims). An orphaned catch-all keeps holding names under its parent for its account, and passes to a later app of the same name in the same account. Detach it before purging. - Rename only applies to never-shipped apps, and the door requires an instance manifest. - Account move leaves claims on the old account, which derive nothing (pre-existing). - Instance delete removes the route but keeps the parked data. The name drops, and comes back if the instance is recreated.

5. Security cases

  • (a) A label colliding with another account's exact claim: the exact claim wins, on every box. A derived name is skipped whenever any claim exists for it. New claims of that kind are refused by the attach guard while the catch-all is live. Claims made before the catch-all keep working, so it cannot take a name away from anyone.
  • (b) A sibling app's apex label: cannot happen. - Only the claim's own app's routes derive. - Sibling apex hosts are not under the parent. - The instance create door refuses a label that names a live or trashed sibling app, and apex.go refuses the name arriving afterwards.
  • (c) A team's name taken before the team exists. - Proved parent: impossible while the catch-all is live, for every label, including a deleted team's. - Unproved parent: the name is protected once the instance exists. A foreign claim made before that wins, which is the first-come rule every claim has. - A same-account exact claim wins on purpose.
  • (d) Races between the claim pass and the instance reconcile: both sides are rebuilt from scratch each pass, and every precedence check is made against the same pass's claim list and route table. A catch-all write racing a foreign attach can let both land; the exact claim then wins, which is the same outcome as (c). A parent released between the door's check and its write is caught by the box's parent re-check.
  • (e) The parent changes hands: the old catch-all goes inert on the box, stops holding names at the door, and the new holder can replace it.
  • (f) A derived alias pointing at another account's route: prevented by three filters, all of which stay: rt.App == c.App, RouteAccount(rt) == c.Account and rt.Provisioned.
  • Abuse of the guard as a squat. - Unproved parent: the guard holds only names of instances that exist, each a real, rate-limited instance. That is the same class of squat as claiming those names directly. - Proved parent: holding every label takes write access to the parent's DNS, which is what owning the domain means. - Public suffixes are refused as parents either way.

7. Review

Reviewed by an Opus agent before landing. Three medium findings were all fixed, each with a test that was watched to fail first where practical:

  1. The guard refused an account's re-attach of its own older claim.
  2. Precedence was decided from decoded bodies, which fails open on a read error.
  3. The guard held every label, which amplified a squat.

Lows taken:

  • the browser form now refuses a channel on a catch-all.

Lows left as they are:

  • Member, not owner, on the API door. The API door admits any person in the account, the browser door only owners. That is the same split ordinary attach has.
  • A reassigned name keeps its old dns_status until the next stamp. Pre-existing for exact claims changing hands.
  • Once-per-process refusal logs do not repeat if a catch-all recovers and fails again.

8. Second review (#233, Paul's agent with Astra)

Two P1s and three P2s. Every one was checked against the code and found real, and all five are fixed with tests that were watched to fail first.

  1. P1: a team's name could be taken before the team exists. Wildcard DNS points every label at the edge, and attach needs no proof. Fixed: a proved parent reserves every label (§2, the attach guard).
  2. P1: the guard checked only the current manifest domain. Fixed: the guard counts the app's instance record keys by first label, across every instance domain (domains.InstanceLabelCounts), with one listing and no record bodies.
  3. P2: a duplicate label routed differently per box. Two instances sharing a label, possible only after an instance-domain change, were each named by the box that held them. Fixed: a label shared by two records derives nothing, on every box alike, and the refusal is logged. - Seen from the same record listing, asked once per catch-all and only when the box has an instance to name. - If the listing fails, the catch-all's existing rows are kept for that pass, never dropped. - The earlier "first in host order wins" text is withdrawn.
  4. P2: an old owner's detach could delete the new owner's catch-all. This happened after the parent changed hands and the catch-all was replaced in place. Fixed: DetachClaim deletes an instance catch-all only at the revision it read (release.DeleteIfMatch). - Ordinary claims are unchanged. They cannot be replaced while held, so the race does not exist for them. - Both production stores (S3, DirStore) support the conditional delete.
  5. P2: a route catch-all on one box beat an instance name on another. The local *.<parent> matched before backhaul was considered. Fixed in the edge: a local match made only through a star key now yields to another box's exact host (homeMap.ExactHome, then backhaul). - That is the single-box precedence, exact then star, extended across the deployment. - An arrived hop and a door call keep the local answer. - A two-box test covers it.

The review's answers to this note's asks:

  • --platform catch-alls: keep refusing them.
  • CLI spelling: agreed.
  • "Delete now" leftovers: filed as their own discussion.

6. Surfaces

  • CLI. The catch-all is spelled carlos domains attach --app <a> '*.<parent>' and carlos domains detach --app <a> '*.<parent>'. - The star is the mode, and no other flag combines with it. - This keeps the meaning of attach <host> as "this hostname now serves this app", and needs no new flag grammar. - --catchall stays the bool capability it is; --catchall instances would have made it a value flag and broken bare --catchall. - carlos domains list shows the catch-all row with mode instances, then each derived name the fleet reports, marked via *.<parent>.
  • API. Same endpoints. The attach answer and list rows gain instances: true, and the list gains via on derived rows. Both are additive.
  • Web UI. The app's Domains & TLS table shows the catch-all and its derived names. The page does not edit wildcard or catchall today, and the catch-all is person-only, so the page lets an owner attach and detach it the same way as any hostname: typing *.<parent> into the attach form.