edges file: delegated customer wildcards are published and loaded on every edge (#305)
A delegated parent (wildcard_domains) lived only in the registry of the box homing its primary, so a region edge never loaded the parent's wildcard PEM the mirror had already put on its disk: pds.woodstar.app terminated on IE and failed TLS through AU/UK/US while woodstar.app itself, an exact alias, worked everywhere after #287. The publisher writes each parent as "*.<parent>" in its own class (wildcard: true, never platform); the fold keeps it; WildcardParents() hands the bare parents to the wildcard manager's delegated source, which unions them with its local rows on every pass — a non-issuer loadCached()s the mirrored PEM, the issuer already minted it — and a delegated entry appearing or leaving wakes the manager. Home's one-label rule dials hosts under the parent as it already did for a catch-all row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6 files changed,
+206
−11
docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md+12 −2internal/edge/homemap.go+38 −7internal/edge/wildcard.go+7 −2internal/edge/wildcard_parents_test.go+76 −0internal/host/edgesfile.go+26 −0internal/host/edgesfile_test.go+47 −0
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 70efa2bd9..349212346 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,16 @@ 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 (#305).* Delegated customer wildcard parents |
| +(`wildcard_domains` rows — a claim with the Wildcard capability, primary |
| +homed here) publish as `"*.<parent>": {role: home, wildcard: true}`, |
| +never `platform`. Every edge folds them: `Home`'s one-label rule dials |
| +hosts under the parent, and the wildcard manager unions |
| +`WildcardParents()` into its delegated set — a non-issuer loads the |
| +parent's mirrored PEM, the issuer minted it — so `pds.customer.example` |
| +terminates on a region edge exactly as on the home box. Precedence is |
| +unchanged: a static or apps-domain parent still beats a delegated one. |
| + |
| *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`, |
| @@ -265,8 +275,8 @@ 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 |
| +alias is NOT published from the alias table — its dial target comes from |
| +the wildcard entry below. 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 |
diff --git a/internal/edge/homemap.go b/internal/edge/homemap.go| index 3ef5cf39a..6da38a2e9 100644 |
| --- a/internal/edge/homemap.go |
| +++ b/internal/edge/homemap.go |
| @@ -141,6 +141,9 @@ type edgesHostView struct { |
| // 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"` |
| + // Wildcard marks a delegated customer wildcard parent's catch-all entry |
| + // (#305): WildcardParents feeds it to the wildcard manager. |
| + Wildcard bool `json:"wildcard,omitempty"` |
| } |
| // homeEntry is one folded answer: where to dial, and which box that is. |
| @@ -151,6 +154,8 @@ type homeEntry struct { |
| platform bool |
| // alias marks an attached custom domain's entry (never platform). |
| alias bool |
| + // wildcard marks a delegated wildcard parent's catch-all entry. |
| + wildcard bool |
| } |
| // homeSnapshot is one poll's result, swapped in whole and immutable once |
| @@ -326,29 +331,55 @@ func (m *homeMap) refreshHosts(ctx context.Context) error { |
| func samePlatformSet(a, b map[string]homeEntry) bool { |
| na, nb := 0, 0 |
| for h, e := range a { |
| - if !parentInput(h, e) { |
| + if !parentInput(h, e) && !wildcardInput(h, e) { |
| continue |
| } |
| na++ |
| - if eb, ok := b[h]; !ok || !parentInput(h, eb) { |
| + if eb, ok := b[h]; !ok || (parentInput(h, e) != parentInput(h, eb)) || (wildcardInput(h, e) != wildcardInput(h, eb)) { |
| return false |
| } |
| } |
| for h, e := range b { |
| - if parentInput(h, e) { |
| + if parentInput(h, e) || wildcardInput(h, e) { |
| nb++ |
| } |
| } |
| return na == nb |
| } |
| -// parentInput is the one filter Hosts and samePlatformSet share: a marked, |
| -// exact (non catch-all) name. A catch-all coming or going cannot move a |
| -// parent, so it must not wake the manager either. |
| +// parentInput is the filter Hosts and samePlatformSet share for ACCOUNT |
| +// parents: a marked, exact (non catch-all) name. wildcardInput is its twin |
| +// for delegated parents (WildcardParents). Anything else coming or going |
| +// cannot move a parent, so it must not wake the manager either. |
| func parentInput(host string, e homeEntry) bool { |
| return e.platform && !strings.HasPrefix(host, "*.") |
| } |
| +func wildcardInput(host string, e homeEntry) bool { |
| + return e.wildcard && strings.HasPrefix(host, "*.") && len(host) > 2 |
| +} |
| + |
| +// WildcardParents is every delegated customer wildcard parent another box |
| +// publishes ("*.<parent>" entries marked wildcard, #305), as bare parents. |
| +// The wildcard manager unions these with its own wildcard_domains rows so |
| +// a region edge loads the parent's mirrored PEM and terminates the |
| +// one-label hosts Home already dials for it. Sorted, nil-safe, nothing for |
| +// an unlabelled box (as Hosts). |
| +func (m *homeMap) WildcardParents() []string { |
| + if m == nil || m.self == "" { |
| + return nil |
| + } |
| + snap := m.snapshot() |
| + out := make([]string, 0) |
| + for h, e := range snap.hosts { |
| + if wildcardInput(h, e) { |
| + out = append(out, strings.TrimPrefix(h, "*.")) |
| + } |
| + } |
| + sort.Strings(out) |
| + return out |
| +} |
| + |
| // Hosts is every exact PLATFORM-class name the deployment's OTHER boxes |
| // home. Sorted, nil-safe. The wildcard manager unions these with this box's |
| // own routes so its parent set is the deployment's, not the box's (#300): |
| @@ -473,7 +504,7 @@ func foldHomes(files map[string]edgesFileView, self string) map[string]homeEntry |
| continue |
| } |
| } |
| - hosts[host] = homeEntry{eip: f.EIP, box: f.Box, platform: h.Platform && !h.Alias, alias: h.Alias} |
| + hosts[host] = homeEntry{eip: f.EIP, box: f.Box, platform: h.Platform && !h.Alias && !h.Wildcard, alias: h.Alias, wildcard: h.Wildcard} |
| } |
| } |
| return hosts |
diff --git a/internal/edge/wildcard.go b/internal/edge/wildcard.go| index 681c3bfa1..c46635e17 100644 |
| --- a/internal/edge/wildcard.go |
| +++ b/internal/edge/wildcard.go |
| @@ -804,7 +804,7 @@ func newWildcardFromEnv(e *Edge, cache autocert.Cache) *wildcardManager { |
| out := append([]string(nil), lastGood...) |
| mu.Unlock() |
| log.Printf("edge: wildcard domains read failed: %v — delegated set unchanged this pass", err) |
| - return out |
| + return append(out, e.homes.Load().WildcardParents()...) |
| } |
| out := make([]string, 0, len(wds)) |
| for _, w := range wds { |
| @@ -813,7 +813,12 @@ func newWildcardFromEnv(e *Edge, cache autocert.Cache) *wildcardManager { |
| mu.Lock() |
| lastGood = out |
| mu.Unlock() |
| - return out |
| + // Plus every delegated parent another box publishes (#305): the |
| + // issuer minted its wildcard, the mirror put the PEM on this disk, |
| + // and refresh's static/apps-domain precedence applies to these |
| + // exactly as to local rows. Read-through, never folded into |
| + // lastGood: the home map is its own last-good. |
| + return append(out, e.homes.Load().WildcardParents()...) |
| } |
| // Hosts are the DEPLOYMENT's (this box's routes plus every name another |
| // box homes, #300), so a region edge derives the account parents of apps |
diff --git a/internal/edge/wildcard_parents_test.go b/internal/edge/wildcard_parents_test.go| index 3a233d6bb..7f18f330c 100644 |
| --- a/internal/edge/wildcard_parents_test.go |
| +++ b/internal/edge/wildcard_parents_test.go |
| @@ -297,3 +297,79 @@ func TestHomeMapRouteBeatsAliasOnCollision(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// #305, reader side: a marked "*.<parent>" entry is a delegated parent for |
| +// the wildcard manager AND a dial target for hosts one label under it; an |
| +// unmarked catch-all (a pre-#305 file, or a route's own catch-all row) is |
| +// only the latter. A wildcard entry appearing wakes the manager. |
| +func TestHomeMapWildcardParents(t *testing.T) { |
| + s := meshTestStore(t) |
| + putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"home","wildcard":true},"*.plain.example":{"role":"home"},"app.acct.apps.test":{"role":"home","platform":true}}}`) |
| + m := newHomeMap(s, "annie") |
| + var woke atomic.Int32 |
| + m.setHostsWake(func() { woke.Add(1) }) |
| + refreshHomeMap(t, m) |
| + waitWoke(t, &woke, 1) |
| + if got := m.WildcardParents(); !reflect.DeepEqual(got, []string{"customer.example"}) { |
| + t.Fatalf("WildcardParents = %v", got) |
| + } |
| + if got := m.Hosts(); !reflect.DeepEqual(got, []string{"app.acct.apps.test"}) { |
| + t.Fatalf("a wildcard entry leaked into Hosts: %v", got) |
| + } |
| + for _, h := range []string{"pds.customer.example", "x.plain.example"} { |
| + if _, box, ok := m.Home(h); !ok || box != "betty" { |
| + t.Errorf("%s: not dialed via the catch-all", h) |
| + } |
| + } |
| + // A second delegated parent lands: wake. |
| + putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"home","wildcard":true},"*.second.example":{"role":"home","wildcard":true},"app.acct.apps.test":{"role":"home","platform":true}}}`) |
| + refreshHomeMap(t, m) |
| + waitWoke(t, &woke, 2) |
| + if got := m.WildcardParents(); !reflect.DeepEqual(got, []string{"customer.example", "second.example"}) { |
| + t.Fatalf("WildcardParents = %v", got) |
| + } |
| + // An unmarked catch-all coming or going still wakes nobody. |
| + putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"home","wildcard":true},"*.second.example":{"role":"home","wildcard":true},"app.acct.apps.test":{"role":"home","platform":true},"*.plain2.example":{"role":"home"}}}`) |
| + refreshHomeMap(t, m) |
| + time.Sleep(20 * time.Millisecond) |
| + if woke.Load() != 2 { |
| + t.Fatalf("an unmarked catch-all woke the manager (%d)", woke.Load()) |
| + } |
| + var unlabelled *homeMap |
| + if unlabelled.WildcardParents() != nil { |
| + t.Fatal("nil map reported parents") |
| + } |
| +} |
| + |
| +// The manager built from env on a region edge unions the remote delegated |
| +// parent: it is in the parent set AND delegated (challenge via the |
| +// delegation zone), alongside its own local rows. |
| +func TestWildcardManagerUnionsRemoteDelegatedParents(t *testing.T) { |
| + e, _, base := remoteHomedEdge(t, nil) |
| + reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { reg.Close() }) |
| + e.reg = reg |
| + if err := reg.UpsertWildcardDomain(registry.WildcardDomain{Host: "local.example", Account: "acct", App: "app"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + putJSON(t, base, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"home","wildcard":true}}}`) |
| + refreshHomeMap(t, e.homes.Load()) |
| + t.Setenv("CARLOS_WILDCARD_DOMAINS", "") |
| + t.Setenv("PLATFORM_APPS_DOMAIN", "apps.test") |
| + t.Setenv("CARLOS_DNS_PROVIDER", "route53") |
| + t.Setenv("CARLOS_ACME_DELEGATION_ZONE", "acme.apps.test") |
| + w := newWildcardFromEnv(e, autocert.DirCache(t.TempDir())) |
| + if w == nil { |
| + t.Fatal("want a manager") |
| + } |
| + w.refresh() |
| + if got := w.currentParents(); !reflect.DeepEqual(got, []string{"customer.example", "local.example"}) { |
| + t.Fatalf("parents = %v", got) |
| + } |
| + if !w.isDelegated("customer.example") || !w.isDelegated("local.example") { |
| + t.Fatal("remote or local delegated parent not marked delegated") |
| + } |
| +} |
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go| index 715c7467d..a45012265 100644 |
| --- a/internal/host/edgesfile.go |
| +++ b/internal/host/edgesfile.go |
| @@ -49,6 +49,11 @@ type edgeHost struct { |
| // 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"` |
| + // Wildcard marks a delegated customer wildcard parent (wildcard_domains, |
| + // #305): the key is "*.<parent>", the box homes everything one label |
| + // under it, and every edge should load the parent's mirrored wildcard |
| + // PEM. Additive (omitempty); never platform. |
| + Wildcard bool `json:"wildcard,omitempty"` |
| } |
| // edgesFile is the wire shape at edgesFileKey(box), schema v1 (spec Part 3): |
| @@ -186,6 +191,27 @@ func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry, |
| hosts[a.Alias] = edgeHost{Role: "home", Alias: true} |
| } |
| + // Delegated customer wildcards (#305): the wildcard_domains rows are the |
| + // parents this box obtains "*.<parent>" for (a claim with the Wildcard |
| + // capability, primary homed here). Published as the catch-all key with |
| + // its own class so a remote edge both dials one-label hosts under it |
| + // (Home's rule) and loads the parent's mirrored wildcard PEM — the two |
| + // halves #302 could not deliver from an alias row alone. Never platform. |
| + wds, err := reg.ListWildcardDomains() |
| + if err != nil { |
| + return fmt.Errorf("edges: list wildcard domains: %w", err) |
| + } |
| + for _, wd := range wds { |
| + if wd.Host == "" || strings.HasPrefix(wd.Host, "*.") { |
| + continue |
| + } |
| + key := "*." + wd.Host |
| + if _, taken := hosts[key]; taken { |
| + continue |
| + } |
| + hosts[key] = edgeHost{Role: "home", Wildcard: true} |
| + } |
| + |
| if eip == "" && *last != nil { |
| var prior edgesFile |
| if err := json.Unmarshal(*last, &prior); err == nil { |
diff --git a/internal/host/edgesfile_test.go b/internal/host/edgesfile_test.go| index b56d0392b..5dbd7447c 100644 |
| --- a/internal/host/edgesfile_test.go |
| +++ b/internal/host/edgesfile_test.go |
| @@ -695,3 +695,50 @@ func TestEdgesFilePublishesPointingAliasesOfServedPrimaries(t *testing.T) { |
| t.Error("a detached alias was still published") |
| } |
| } |
| + |
| +// #305: delegated customer wildcard parents publish as "*.<parent>" in their |
| +// own class — never platform, never over an existing key. |
| +func TestEdgesFilePublishesDelegatedWildcardParents(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: "*.taken.example", Kind: "service", Addr: "10.0.0.1:3001", Status: "ready", Platform: true}); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, w := range []registry.WildcardDomain{ |
| + {Host: "customer.example", Account: "acct", App: "app"}, |
| + {Host: "*.odd.example", Account: "acct", App: "app"}, // never written by the pass; refused here too |
| + {Host: "taken.example", Account: "acct", App: "app"}, // a real route owns the key |
| + } { |
| + if err := reg.UpsertWildcardDomain(w); 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) |
| + } |
| + 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"` |
| + Wildcard bool `json:"wildcard"` |
| + } `json:"hosts"` |
| + } |
| + if err := json.Unmarshal(body, &f); err != nil { |
| + t.Fatal(err) |
| + } |
| + h, ok := f.Hosts["*.customer.example"] |
| + if !ok || h.Role != "home" || !h.Wildcard || h.Platform { |
| + t.Fatalf("*.customer.example = %+v, %v; want home, wildcard, not platform", h, ok) |
| + } |
| + if _, ok := f.Hosts["*.*.odd.example"]; ok { |
| + t.Error("a malformed wildcard_domains row was published") |
| + } |
| + if h := f.Hosts["*.taken.example"]; !h.Platform || h.Wildcard { |
| + t.Errorf("a wildcard entry displaced a real route's key: %+v", h) |
| + } |
| +} |