edges file: aliases marked on the wire, routes win collisions, publish only pointing aliases (#287 review)
Review: on a non-home edge an alias entry was indistinguishable from a route entry, so a tenant attaching a hostname that is an operator's route on ANOTHER box could capture it fleet-wide on a box-name coin flip; the raw alias set is tenant-chosen and unbounded (attach needs no DNS proof), which handed a tenant an unbounded set of names the shared issuer would order for; and a published catch-all alias was a dial target no remote edge could terminate. Now: entries carry alias:true (additive) and the fold lets a route beat an alias whatever the sort order, an alias can never carry the platform mark; only an alias whose DNS the sweep stamped 'pointing' publishes; catch-all aliases stay unpublished (served on their home box only, as before). Also: the plain alias branch shape-checks like the wildcard branch, the tunnel host joins the console's deny list, and the spec amendment states the two residuals (#286 order start, no cert purge on detach) instead of overclaiming.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7 files changed,
+139
−55
docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md+17 −10internal/console/apidomains.go+9 −0internal/domains/aliases.go+5 −0internal/edge/homemap.go+14 −3internal/edge/wildcard_parents_test.go+22 −1internal/host/edgesfile.go+23 −8internal/host/edgesfile_test.go+49 −33
diff --git a/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md b/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md| index 89d2f2366..e4d111856 100644 |
| --- a/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md |
| +++ b/docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md |
| @@ -256,16 +256,23 @@ follow-up issue, with one constraint stated in advance: an alias whose |
| host is a `*.<parent>` catch-all must stay wildcard-cert-only if it is |
| ever published — the same #180 bound this amendment refuses to widen. |
| -*Amendment 2026-08-26 (#287): done.* The publisher writes each alias |
| -whose primary is a served row under the alias's own name (`role: |
| -"home"`, never `platform`), so `Home` backhauls it and `KnownHost` |
| -admits it on every edge — the issuer orders its per-host cert whichever |
| -edge the validation lands on. A `*.<parent>` catch-all alias goes |
| -verbatim, exactly as the routes' catch-all rows do: `Home`'s one-label |
| -rule dials it, `KnownHost` still refuses it. Custom domains on a |
| -multi-edge deployment therefore work through any edge — `CNAME` to |
| -`edge.<apps-domain>` inherits steering and failover; an apex needs the |
| -current edge-IP set (#299). |
| +*Amendment 2026-08-26 (#287).* The publisher writes each exact alias |
| +whose primary is a served row AND whose DNS the sweep has stamped |
| +`pointing` under the alias's own name (`role: "home"`, `alias: true`, |
| +never `platform`), so `Home` backhauls it and `KnownHost` admits it on |
| +every edge — any edge answers its validations. Bounds: the `pointing` |
| +gate keeps the issuer's orderable set to names that actually resolve |
| +here (attach needs no DNS proof, so the raw alias set is tenant-chosen |
| +and unbounded); the fold lets a route beat an alias on a cross-box name |
| +collision, whatever the boxes' sort order; a `*.<parent>` catch-all |
| +alias is NOT published — its delegated wildcard parent is box-local, so |
| +a remote edge could dial it but never terminate it. Two residuals stay |
| +as they were: an ORDER still only starts from a hello reaching the |
| +issuer (#286), so a steered custom domain whose hellos never land there |
| +stays certless; and nothing purges `control/certs/` on detach. Net for |
| +#299: a customer subdomain `CNAME → edge.<apps-domain>` works through |
| +any edge with steering and failover; an apex needs the current edge-IP |
| +set. |
| **Order amplification, accepted.** The set of names a stranger can induce |
| a fresh ACME order for at the issuer grows from one box's routes to every |
diff --git a/internal/console/apidomains.go b/internal/console/apidomains.go| index 0b1ee0fe4..04a6d099a 100644 |
| --- a/internal/console/apidomains.go |
| +++ b/internal/console/apidomains.go |
| @@ -40,6 +40,7 @@ import ( |
| "log" |
| "net/http" |
| "net/url" |
| + "os" |
| "sort" |
| "strconv" |
| "strings" |
| @@ -97,6 +98,14 @@ func (s *Server) deniedDomains() []string { |
| deny = append(deny, h) |
| } |
| } |
| + // The fleet hub's reserved hostname (CARLOS_TUNNEL_HOST, the edge's env — |
| + // host.env is shared with the console on the same box) is Allowed by the |
| + // edge but is no registry route, so nothing else here names it; a tenant |
| + // must not attach it (#151 said "MUST be in CONSOLE_DENY_DOMAINS" — |
| + // this makes it so without relying on the operator remembering). |
| + if h := normalizeHost(os.Getenv("CARLOS_TUNNEL_HOST")); h != "" { |
| + deny = append(deny, h) |
| + } |
| return deny |
| } |
diff --git a/internal/domains/aliases.go b/internal/domains/aliases.go| index 27e0e7c61..79b52473f 100644 |
| --- a/internal/domains/aliases.go |
| +++ b/internal/domains/aliases.go |
| @@ -65,6 +65,11 @@ func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Regist |
| c.Host, c.Route, release.RouteAccount(rt), rt.App, c.Account, c.App) |
| } else if _, taken := byHost[c.Host]; taken { |
| log.Printf("domains: alias %s skipped: a real route owns that host", c.Host) |
| + } else if !validHostShape(c.Host) { |
| + // Same check the wildcard branch below applies: both doors |
| + // normalise, but this row now becomes a key in a file every |
| + // edge folds (#287), so a hand-written claim must not reach it. |
| + log.Printf("domains: alias %s refused: invalid host shape", c.Host) |
| } else { |
| desired[c.Host] = true |
| if err := reg.UpsertAlias(registry.RouteAlias{ |
diff --git a/internal/edge/homemap.go b/internal/edge/homemap.go| index d8931fcf2..3ef5cf39a 100644 |
| --- a/internal/edge/homemap.go |
| +++ b/internal/edge/homemap.go |
| @@ -138,6 +138,9 @@ type edgesHostView struct { |
| // existed, which reads as "not platform" — fail closed for the one |
| // consumer that cares (Hosts, the wildcard parent set). |
| Platform bool `json:"platform,omitempty"` |
| + // Alias marks an attached custom domain (#287). On a name collision |
| + // across boxes a route entry beats an alias entry (foldHomes). |
| + Alias bool `json:"alias,omitempty"` |
| } |
| // homeEntry is one folded answer: where to dial, and which box that is. |
| @@ -146,6 +149,8 @@ type homeEntry struct { |
| box string |
| // platform carries the publisher's class mark through the fold. |
| platform bool |
| + // alias marks an attached custom domain's entry (never platform). |
| + alias bool |
| } |
| // homeSnapshot is one poll's result, swapped in whole and immutable once |
| @@ -459,10 +464,16 @@ func foldHomes(files map[string]edgesFileView, self string) map[string]homeEntry |
| if host == "" || h.Role != edgeRoleHome { |
| continue |
| } |
| - if _, dup := hosts[host]; dup { |
| - continue // first box in sorted order wins, everywhere |
| + if prev, dup := hosts[host]; dup { |
| + // First box in sorted order wins, everywhere — except that a |
| + // ROUTE beats an ALIAS whatever the order: a tenant attaching |
| + // an operator's hostname on one box must not capture it on |
| + // every other (#287). |
| + if !(prev.alias && !h.Alias) { |
| + continue |
| + } |
| } |
| - hosts[host] = homeEntry{eip: f.EIP, box: f.Box, platform: h.Platform} |
| + hosts[host] = homeEntry{eip: f.EIP, box: f.Box, platform: h.Platform && !h.Alias, alias: h.Alias} |
| } |
| } |
| return hosts |
diff --git a/internal/edge/wildcard_parents_test.go b/internal/edge/wildcard_parents_test.go| index 6b234ee3c..3a233d6bb 100644 |
| --- a/internal/edge/wildcard_parents_test.go |
| +++ b/internal/edge/wildcard_parents_test.go |
| @@ -257,7 +257,7 @@ func TestWildcardRefreshIsSerialized(t *testing.T) { |
| // is never an exact admission (#180). |
| func TestAliasEntriesBackhaulAndAdmitButDeriveNoParent(t *testing.T) { |
| e, _, base := remoteHomedEdge(t, nil) |
| - putJSON(t, base, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"www.customer.com":{"role":"home","platform":false},"*.customer.com":{"role":"home","platform":false},"app.acct.apps.test":{"role":"home","platform":true}}}`) |
| + putJSON(t, base, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"www.customer.com":{"role":"home","platform":false,"alias":true},"*.customer.com":{"role":"home","platform":false,"alias":true},"app.acct.apps.test":{"role":"home","platform":true}}}`) |
| hm := e.homes.Load() |
| refreshHomeMap(t, hm) |
| if _, box, ok := hm.Home("www.customer.com"); !ok || box != "betty" { |
| @@ -276,3 +276,24 @@ func TestAliasEntriesBackhaulAndAdmitButDeriveNoParent(t *testing.T) { |
| t.Fatalf("customer names leaked into the parent input: %v", got) |
| } |
| } |
| + |
| +// A route beats an alias on a cross-box name collision whatever the |
| +// boxes' sort order (#287): a tenant attaching an operator's hostname |
| +// captures nothing beyond its own box. And an alias can never smuggle the |
| +// platform mark. |
| +func TestHomeMapRouteBeatsAliasOnCollision(t *testing.T) { |
| + for _, order := range [][2]string{{"aaa", "zzz"}, {"zzz", "aaa"}} { |
| + routeBox, aliasBox := order[0], order[1] |
| + s := meshTestStore(t) |
| + putJSON(t, s, "control/edges/"+routeBox+".json", `{"v":1,"box":"`+routeBox+`","eip":"1.1.1.1","hosts":{"status.platform.test":{"role":"home","platform":false}}}`) |
| + putJSON(t, s, "control/edges/"+aliasBox+".json", `{"v":1,"box":"`+aliasBox+`","eip":"2.2.2.2","hosts":{"status.platform.test":{"role":"home","platform":true,"alias":true}}}`) |
| + m := newHomeMap(s, "self") |
| + refreshHomeMap(t, m) |
| + if _, box, ok := m.Home("status.platform.test"); !ok || box != routeBox { |
| + t.Fatalf("order %v: home = %s; want the route's box %s", order, box, routeBox) |
| + } |
| + if got := m.Hosts(); len(got) != 0 { |
| + t.Fatalf("order %v: an alias entry reached the parent input: %v", order, got) |
| + } |
| + } |
| +} |
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go| index 27834115c..bfa9e6d18 100644 |
| --- a/internal/host/edgesfile.go |
| +++ b/internal/host/edgesfile.go |
| @@ -43,6 +43,11 @@ type edgeHost struct { |
| // homing nothing platform-class. A reader seeing no flag treats the host |
| // as unclassed and derives no parent from it (#300). |
| Platform bool `json:"platform"` |
| + // Alias marks an attached custom domain (route_aliases, #287) as opposed |
| + // to a registry route. Additive (omitempty). The reader's fold lets a |
| + // route beat an alias on a name collision across boxes: a tenant |
| + // attaching an operator's hostname must never capture it fleet-wide. |
| + Alias bool `json:"alias,omitempty"` |
| } |
| // edgesFile is the wire shape at edgesFileKey(box), schema v1 (spec Part 3): |
| @@ -132,18 +137,28 @@ func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry, |
| // a served row above, under the alias's own name — the reader wants |
| // exact names, and every reader already does the right thing with one: |
| // Home dials the home box with the alias as SNI, and the home box's own |
| - // upstream map serves it. Never platform-class: a customer's name |
| - // derives no wildcard parent. A "*.parent" catch-all alias goes verbatim, |
| - // as the routes' catch-all rows do — Home's one-label rule dials it, |
| - // KnownHost refuses it (PR #180's bound: a wildcard label is never an |
| - // exact cert admission). An alias shadowed by a real route is the |
| - // route's, as edge.Reload rules. |
| + // upstream map serves it. Bounds, each doing work: |
| + // |
| + // - only an alias whose DNS the sweep has stamped "pointing" (at one of |
| + // PLATFORM_EDGE_IPS). Attach needs no DNS proof, so the alias set is |
| + // tenant-chosen and unbounded; publishing every one would hand a |
| + // tenant an unbounded set of names the shared issuer will order for |
| + // (#277's amplification envelope covers routes, which the platform |
| + // provisions). A name that actually resolves to this deployment is |
| + // the name whose validations must be answerable everywhere; |
| + // - never platform-class: a customer's name derives no wildcard parent; |
| + // - marked alias: the reader lets a real route on another box beat it; |
| + // - no "*.parent" catch-all alias: a remote edge could dial it but has |
| + // no cert for it (delegated wildcard parents are box-local), so it |
| + // stays exactly as before — served on its home box only; |
| + // - an alias shadowed by a real route here is the route's, as |
| + // edge.Reload rules. |
| aliases, err := reg.ListAliases() |
| if err != nil { |
| return fmt.Errorf("edges: list aliases: %w", err) |
| } |
| for _, a := range aliases { |
| - if a.Alias == "" { |
| + if a.Alias == "" || strings.HasPrefix(a.Alias, "*.") || a.DNSStatus != "pointing" { |
| continue |
| } |
| if _, served := hosts[a.Host]; !served { |
| @@ -152,7 +167,7 @@ func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry, |
| if _, taken := hosts[a.Alias]; taken { |
| continue |
| } |
| - hosts[a.Alias] = edgeHost{Role: "home"} |
| + hosts[a.Alias] = edgeHost{Role: "home", Alias: true} |
| } |
| if eip == "" && *last != nil { |
diff --git a/internal/host/edgesfile_test.go b/internal/host/edgesfile_test.go| index d167216d3..b29c01bbe 100644 |
| --- a/internal/host/edgesfile_test.go |
| +++ b/internal/host/edgesfile_test.go |
| @@ -614,11 +614,14 @@ func TestEdgesFileMarksPlatformRows(t *testing.T) { |
| } |
| // #287: attached custom domains are published under their own names — |
| -// only when their primary is served, never platform-class, catch-alls |
| -// verbatim, and never over a real route. |
| -func TestEdgesFilePublishesAliasesOfServedPrimaries(t *testing.T) { |
| +// only when their primary is served AND their DNS points here, marked as |
| +// aliases, never platform-class, catch-alls never, and never over a real |
| +// route. Detaching one republishes without it. |
| +func TestEdgesFilePublishesPointingAliasesOfServedPrimaries(t *testing.T) { |
| s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t) |
| - seedRoute(t, reg, "app.acct.apps.test") |
| + if err := reg.Upsert(registry.Route{Host: "app.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Platform: true}); err != nil { |
| + t.Fatal(err) |
| + } |
| if err := reg.Upsert(registry.Route{Host: "pending.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3001", Status: "pending"}); err != nil { |
| t.Fatal(err) |
| } |
| @@ -626,45 +629,58 @@ func TestEdgesFilePublishesAliasesOfServedPrimaries(t *testing.T) { |
| t.Fatal(err) |
| } |
| for _, a := range []registry.RouteAlias{ |
| - {Alias: "www.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app"}, |
| - {Alias: "*.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app"}, |
| - {Alias: "orphan.example.org", Host: "pending.acct.apps.test", Account: "acct", App: "pending"}, |
| - {Alias: "real.example.net", Host: "app.acct.apps.test", Account: "acct", App: "app"}, // shadowed |
| + {Alias: "www.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app", DNSStatus: "pointing"}, |
| + {Alias: "unpointed.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app", DNSStatus: "elsewhere"}, |
| + {Alias: "fresh.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app"}, |
| + {Alias: "*.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app", DNSStatus: "pointing"}, |
| + {Alias: "orphan.example.org", Host: "pending.acct.apps.test", Account: "acct", App: "pending", DNSStatus: "pointing"}, |
| + {Alias: "real.example.net", Host: "app.acct.apps.test", Account: "acct", App: "app", DNSStatus: "pointing"}, // shadowed |
| } { |
| if err := reg.UpsertAlias(a); err != nil { |
| t.Fatal(err) |
| } |
| } |
| - var lastBody []byte |
| - if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &lastBody); err != nil { |
| - t.Fatal(err) |
| + type hostView struct { |
| + Role string `json:"role"` |
| + Platform bool `json:"platform"` |
| + Alias bool `json:"alias"` |
| } |
| - body, _, err := s.Get(context.Background(), "control/edges/annie.json") |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - var f struct { |
| - Hosts map[string]struct { |
| - Role string `json:"role"` |
| - Platform bool `json:"platform"` |
| - } `json:"hosts"` |
| + publish := func() map[string]hostView { |
| + t.Helper() |
| + var lastBody []byte |
| + if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &lastBody); err != nil { |
| + t.Fatal(err) |
| + } |
| + body, _, err := s.Get(context.Background(), "control/edges/annie.json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var f struct { |
| + Hosts map[string]hostView `json:"hosts"` |
| + } |
| + if err := json.Unmarshal(body, &f); err != nil { |
| + t.Fatal(err) |
| + } |
| + return f.Hosts |
| } |
| - if err := json.Unmarshal(body, &f); err != nil { |
| - t.Fatal(err) |
| + hosts := publish() |
| + h, ok := hosts["www.customer.com"] |
| + if !ok || h.Role != "home" || !h.Alias || h.Platform { |
| + t.Errorf("www.customer.com = %+v, %v; want home, alias, not platform", h, ok) |
| } |
| - for _, alias := range []string{"www.customer.com", "*.customer.com"} { |
| - h, ok := f.Hosts[alias] |
| - if !ok || h.Role != "home" { |
| - t.Errorf("%s: not published as home (%+v, %v)", alias, h, ok) |
| - } |
| - if h.Platform { |
| - t.Errorf("%s: a customer alias must never be platform-class", alias) |
| + for _, absent := range []string{"unpointed.customer.com", "fresh.customer.com", "*.customer.com", "orphan.example.org"} { |
| + if _, ok := hosts[absent]; ok { |
| + t.Errorf("%s must not be published", absent) |
| } |
| } |
| - if _, ok := f.Hosts["orphan.example.org"]; ok { |
| - t.Error("an alias of an unserved primary was published") |
| + if h := hosts["real.example.net"]; !h.Platform || h.Alias { |
| + t.Errorf("an alias colliding with a real route displaced the route's entry: %+v", h) |
| + } |
| + // Detach: gone on the next pass. |
| + if err := reg.DeleteAlias("www.customer.com"); err != nil { |
| + t.Fatal(err) |
| } |
| - if h := f.Hosts["real.example.net"]; !h.Platform { |
| - t.Error("an alias colliding with a real route displaced the route's entry") |
| + if _, ok := publish()["www.customer.com"]; ok { |
| + t.Error("a detached alias was still published") |
| } |
| } |