carlos / platform Public

Merge pull request #302 from carlosframework/aliases-in-edges-file

edges file: publish attached custom domains under their own names (#287)
Paul Campbell pushed by paul@keymail.dev 5e9b918cc546feb9ae3c83ddc6857898ae0a4ee3 parent 6340c9eparent 2130c55
8 files changed, +245 −10
  • cmd/console/main.go +14 −5
  • docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md +18 −0
  • internal/console/apidomains.go +3 −2
  • internal/domains/aliases.go +6 −0
  • internal/edge/homemap.go +14 −3
  • internal/edge/wildcard_parents_test.go +47 −0
  • internal/host/edgesfile.go +60 −0
  • internal/host/edgesfile_test.go +83 −0
diff --git a/cmd/console/main.go b/cmd/console/main.go
index 335177c32..a70e25246 100644
--- a/cmd/console/main.go
+++ b/cmd/console/main.go
@@ -678,11 +678,11 @@ func redirectHosts() []string {
// denyDomains parses CONSOLE_DENY_DOMAINS (comma-separated) — hostnames, and
// everything under them, that `carlos domains attach` refuses (issue #151).
// This is the operator's list of names the console cannot derive on its own:
-// the edge's reserved tunnel hostname, retired console origins, any other
-// platform-managed domain. The console's own origin host and
-// PLATFORM_APPS_DOMAIN are added by the door itself (deniedDomains,
-// internal/console/apidomains.go), so an unset variable is a legal — and
-// still safe — deployment. Blank entries are dropped: an empty string in the
+// retired console origins, any other platform-managed domain. The edge's
+// reserved tunnel hostname is folded in below from CARLOS_TUNNEL_HOST; the
+// console's own origin host and PLATFORM_APPS_DOMAIN are added by the door
+// itself (deniedDomains, internal/console/apidomains.go), so an unset
+// variable is a legal — and still safe — deployment. Blank entries are dropped: an empty string in the
// list would be a deny entry no hostname could avoid matching.
func denyDomains() []string {
var out []string
@@ -691,6 +691,15 @@ func denyDomains() []string {
out = append(out, d)
}
}
+ // The fleet hub's reserved hostname (CARLOS_TUNNEL_HOST — the edge's
+ // variable, but host.env is one file per box) is Allowed by the edge and
+ // is no registry route, so no derived deny entry names it; a tenant must
+ // not attach it. #151 made it the operator's job to list; folding it in
+ // here makes the MUST hold without anyone remembering. Env is read at
+ // this layer only, as every other console setting is.
+ if h := strings.ToLower(strings.TrimSpace(os.Getenv("CARLOS_TUNNEL_HOST"))); h != "" {
+ out = append(out, strings.TrimSuffix(h, "."))
+ }
return out
}
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 8bf68d77c..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,6 +256,24 @@ 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).* 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
box's routes. Each such order for a name whose DNS is dead burns that
diff --git a/internal/console/apidomains.go b/internal/console/apidomains.go
index 0b1ee0fe4..192c0f220 100644
--- a/internal/console/apidomains.go
+++ b/internal/console/apidomains.go
@@ -52,8 +52,9 @@ import (
)
// deniedDomains is the deny list every attach is checked against: the
-// operator's own CONSOLE_DENY_DOMAINS entries, plus the three names this
-// console can derive without being told (spec §5: "all derived from
+// operator's CONSOLE_DENY_DOMAINS entries (cmd/console folds the tunnel
+// hostname into those), plus the names this console can derive without
+// being told (spec §5: "all derived from
// console/deployment config, not hardcoded").
//
// The derived three are not optional and not the operator's to forget:
diff --git a/internal/domains/aliases.go b/internal/domains/aliases.go
index 27e0e7c61..1a11a9b1a 100644
--- a/internal/domains/aliases.go
+++ b/internal/domains/aliases.go
@@ -65,6 +65,12 @@ 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 n, ok := NormalizeAliasHost(c.Host); !ok || n != c.Host {
+ // The DOOR's own check (NormalizeAliasHost: shape plus an
+ // interior dot, already lower-case): 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: not a normalised hostname", 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 a97052123..3a233d6bb 100644
--- a/internal/edge/wildcard_parents_test.go
+++ b/internal/edge/wildcard_parents_test.go
@@ -250,3 +250,50 @@ func TestWildcardRefreshIsSerialized(t *testing.T) {
t.Fatalf("parents = %v after generation %d", got, final)
}
}
+
+// #287, reader side: an attached custom domain published under its own
+// name (never platform-class) backhauls, passes the cert gates' exact
+// admission, and derives no wildcard parent; its catch-all form dials but
+// 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,"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" {
+ t.Fatal("alias entry does not backhaul")
+ }
+ if _, _, ok := hm.Home("shop.customer.com"); !ok {
+ t.Fatal("catch-all alias does not dial one label under it")
+ }
+ if !e.deploymentHost("www.customer.com") {
+ t.Fatal("alias entry not admitted by the cert gates")
+ }
+ if e.deploymentHost("*.customer.com") || e.deploymentHost("shop.customer.com") {
+ t.Fatal("a catch-all alias must never be an exact cert admission (#180)")
+ }
+ if got := hm.Hosts(); !reflect.DeepEqual(got, []string{"app.acct.apps.test"}) {
+ 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 e40f4bea8..715c7467d 100644
--- a/internal/host/edgesfile.go
+++ b/internal/host/edgesfile.go
@@ -9,6 +9,7 @@ import (
"math/rand/v2"
"os"
"strings"
+ "sync"
"time"
"github.com/carlosframework/platform/internal/registry"
@@ -43,6 +44,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):
@@ -98,6 +104,10 @@ type edgesFile struct {
// of the change gate like every other field except Generation: a region
// relabel is a real change and must write, not be swallowed as a
// timestamp-only refresh would be.
+// withheldAliasSaid throttles the not-published line in edgesFilePass to
+// once per alias per process (cleared when the alias publishes).
+var withheldAliasSaid sync.Map
+
func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry, box, eip, region string, last *[]byte) error {
routes, err := reg.List()
if err != nil {
@@ -125,6 +135,56 @@ func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry,
// (the leading dot of the first label only) is tried next.
hosts[r.Host] = edgeHost{Role: "home", Platform: r.Platform || r.Provisioned}
}
+ // Attached custom domains (#151) live in route_aliases, not routes, and
+ // were absent from this file (#287): a non-home edge neither backhauled
+ // them (Home missed) nor answered their validations or let the issuer
+ // order for them (KnownHost missed). Publish each alias whose primary is
+ // 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. 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 == "" || strings.HasPrefix(a.Alias, "*.") {
+ continue
+ }
+ if _, served := hosts[a.Host]; !served {
+ continue
+ }
+ if a.DNSStatus != "pointing" {
+ // Withheld, and said so ONCE per name per process: an alias
+ // whose DNS resolves to an address outside PLATFORM_EDGE_IPS
+ // (a region EIP missing from that list is the usual cause) is
+ // otherwise served on its home box only, silently.
+ if _, said := withheldAliasSaid.LoadOrStore(a.Alias, true); !said {
+ log.Printf("edges: alias %s not published — dns_status %q, want \"pointing\" (does PLATFORM_EDGE_IPS list every armed edge?)", a.Alias, a.DNSStatus)
+ }
+ continue
+ }
+ withheldAliasSaid.Delete(a.Alias)
+ if _, taken := hosts[a.Alias]; taken {
+ continue
+ }
+ hosts[a.Alias] = edgeHost{Role: "home", Alias: true}
+ }
if eip == "" && *last != nil {
var prior edgesFile
diff --git a/internal/host/edgesfile_test.go b/internal/host/edgesfile_test.go
index 79966559a..b56d0392b 100644
--- a/internal/host/edgesfile_test.go
+++ b/internal/host/edgesfile_test.go
@@ -3,6 +3,8 @@ package host
import (
"context"
"encoding/json"
+ "log"
+ "os"
"path/filepath"
"strings"
"testing"
@@ -612,3 +614,84 @@ func TestEdgesFileMarksPlatformRows(t *testing.T) {
t.Errorf("mark not emitted for an unclassed row: %s", body)
}
}
+
+// #287: attached custom domains are published under their own names —
+// 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)
+ 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)
+ }
+ if err := reg.Upsert(registry.Route{Host: "real.example.net", Kind: "service", Addr: "10.0.0.1:3002", Status: "ready", Platform: true}); err != nil {
+ t.Fatal(err)
+ }
+ for _, a := range []registry.RouteAlias{
+ {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)
+ }
+ }
+ type hostView struct {
+ Role string `json:"role"`
+ Platform bool `json:"platform"`
+ Alias bool `json:"alias"`
+ }
+ 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
+ }
+ var buf strings.Builder
+ log.SetOutput(&buf)
+ t.Cleanup(func() { log.SetOutput(os.Stderr) })
+ 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 _, 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 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)
+ }
+ // A withheld alias says so once per process, not once per pass.
+ publish()
+ publish()
+ if n := strings.Count(buf.String(), "unpointed.customer.com not published"); n != 1 {
+ t.Errorf("withheld line logged %d times over 3 passes; want 1:\n%s", n, buf.String())
+ }
+ // Detach: gone on the next pass.
+ if err := reg.DeleteAlias("www.customer.com"); err != nil {
+ t.Fatal(err)
+ }
+ if _, ok := publish()["www.customer.com"]; ok {
+ t.Error("a detached alias was still published")
+ }
+}