carlos / platform Public

Merge pull request #311 from carlosframework/delegated-wildcards-deployment

edges file: delegated customer wildcards are published and loaded on every edge (#305)
Paul Campbell pushed by paul@keymail.dev 2aaf747be4bf179015b047853d04ea8da5aa89ab parent 845a850parent f821855
6 files changed, +463 −21
  • docs/superpowers/specs/2026-08-20-multi-region-ingress-design.md +19 −2
  • internal/edge/homemap.go +93 −17
  • internal/edge/wildcard.go +60 −2
  • internal/edge/wildcard_parents_test.go +163 −0
  • internal/host/edgesfile.go +55 −0
  • internal/host/edgesfile_test.go +73 −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..3924eed02 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,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 (#305).* Delegated customer wildcard parents
+(`wildcard_domains` rows — a claim with the Wildcard capability, primary
+homed here) publish as `"*.<parent>": {wildcard: true}` with role `home`
+when this box serves the catch-all (a Catchall claim, alias row at a
+served primary) and role `wildcard` — the cert half alone, no dial
+target — otherwise; never `platform`. Every edge folds them: the fold
+MERGES classes on a cross-box collision and picks one dial target
+(route beats alias, any beats none), `Home` dials only entries with a
+dial half, and the wildcard manager unions `WildcardParents()` into its
+delegated set, capped at the per-box table's 32 fleet-wide (local rows
+first, overflow logged) — 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. `CARLOS_ACME_DELEGATION_ZONE`
+is therefore fleet-wide env: a box without it logs when remote parents
+exist, since it could win the issuer lease.
+
*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 +282,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..aa675bfd8 100644
--- a/internal/edge/homemap.go
+++ b/internal/edge/homemap.go
@@ -105,6 +105,11 @@ const (
// not take its whole file — or the poll — down with it.
const edgeRoleHome = "home"
+// edgeRoleWildcard is the cert-only half of a delegated wildcard parent
+// (#305): the parent's PEM should be loaded everywhere, but this box does
+// not serve the catch-all, so the entry is no dial target.
+const edgeRoleWildcard = "wildcard"
+
// homeMapInterval / homeMapTimeout / homeMapBootTimeout mirror the cert
// mirror's cadence and budgets exactly (certsync.go), for the same reasons: a
// home move is a placement event that converge heals, the poll runs on its own
@@ -141,6 +146,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 +159,11 @@ 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
+ // dial reports whether the entry is a backhaul target (role home). A
+ // cert-only wildcard entry folds with dial false and Home skips it.
+ dial bool
}
// homeSnapshot is one poll's result, swapped in whole and immutable once
@@ -326,29 +339,62 @@ 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 {
+ if !e.wildcard || !strings.HasPrefix(host, "*.") {
+ return false
+ }
+ parent := host[2:]
+ // The shape a local wildcard_domains row passed (validHostShape at
+ // reconcile), applied to a remote one: a real dotted name, no further
+ // wildcard label, nothing a challenge FQDN could not be built from.
+ return parent != "" && strings.Contains(parent, ".") && !strings.ContainsAny(parent, "*/ \t")
+}
+
+// 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):
@@ -457,23 +503,53 @@ func foldHomes(files map[string]edgesFileView, self string) map[string]homeEntry
hosts := make(map[string]homeEntry)
for _, k := range keys {
f := files[k]
- if f.Box == "" || f.Box == self || f.EIP == "" {
+ if f.Box == "" || f.Box == self {
continue
}
for host, h := range f.Hosts {
- if host == "" || h.Role != edgeRoleHome {
+ if host == "" {
continue
}
- 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) {
+ switch h.Role {
+ case edgeRoleHome:
+ // A dial target needs an address. A file with no EIP (a box
+ // with no discoverable public IP) still carries its cert
+ // halves below; its dial halves are simply not dialable.
+ if f.EIP == "" {
+ if !h.Wildcard || !strings.HasPrefix(host, "*.") {
+ continue
+ }
+ h.Role = edgeRoleWildcard
+ }
+ case edgeRoleWildcard:
+ // The cert-only role is defined for marked catch-all keys
+ // alone; on an exact key it would be a KnownHost admission
+ // with no dial behind it — refused.
+ if !h.Wildcard || !strings.HasPrefix(host, "*.") {
continue
}
+ default:
+ continue
+ }
+ in := homeEntry{eip: f.EIP, box: f.Box, platform: h.Platform && !h.Alias && !h.Wildcard, alias: h.Alias, wildcard: h.Wildcard, dial: h.Role == edgeRoleHome}
+ prev, dup := hosts[host]
+ if !dup {
+ hosts[host] = in
+ continue
+ }
+ // A collision merges CLASSES and picks ONE dial target: the
+ // classes are facts about the name (a delegated parent's PEM
+ // must load everywhere even if some other box also routes the
+ // key), the dial target is a choice. First box in sorted order
+ // wins the dial — except that a ROUTE beats an ALIAS whatever
+ // the order (#287), and any dial target beats none.
+ merged := prev
+ merged.platform = prev.platform || in.platform
+ merged.wildcard = prev.wildcard || in.wildcard
+ if in.dial && (!prev.dial || (prev.alias && !in.alias)) {
+ merged.eip, merged.box, merged.alias, merged.dial = in.eip, in.box, in.alias, true
}
- hosts[host] = homeEntry{eip: f.EIP, box: f.Box, platform: h.Platform && !h.Alias, alias: h.Alias}
+ hosts[host] = merged
}
}
return hosts
@@ -505,11 +581,11 @@ func (m *homeMap) Home(host string) (eip, box string, ok bool) {
return "", "", false
}
hosts := m.snapshot().hosts
- if h, ok := hosts[host]; ok {
+ if h, ok := hosts[host]; ok && h.dial {
return h.eip, h.box, true
}
if i := strings.IndexByte(host, '.'); i > 0 {
- if h, ok := hosts["*"+host[i:]]; ok {
+ if h, ok := hosts["*"+host[i:]]; ok && h.dial {
return h.eip, h.box, true
}
}
diff --git a/internal/edge/wildcard.go b/internal/edge/wildcard.go
index 681c3bfa1..eb78a74e9 100644
--- a/internal/edge/wildcard.go
+++ b/internal/edge/wildcard.go
@@ -44,6 +44,7 @@ import (
"sort"
"strings"
"sync"
+ "sync/atomic"
"time"
"golang.org/x/crypto/acme"
@@ -130,6 +131,58 @@ func newWildcardManager(staticDomains []string, appsDomain string, hosts func()
}
}
+// maxDelegatedParents bounds the delegated set this box will carry —
+// local rows plus every parent other boxes publish (#305). The per-box
+// cap on wildcard_domains (internal/domains, 32) exists because every
+// delegated parent shares one ACME account's new-order budget; a fleet's
+// union must not multiply that by the box count, or one broken delegation
+// zone would have the issuer retrying 32×N orders an hour. Local rows come
+// first; the overflow is logged, once per pass, never silently dropped.
+const maxDelegatedParents = 32
+
+// unionDelegated is the delegated source's merge: local rows, then remote
+// parents, capped. A remote parent on a box with NO delegation zone is a
+// rollout mistake worth a line — this box could win the issuer lease and
+// would then fail every order for it against the customer's own zone.
+func unionDelegated(local, remote []string, zone string) []string {
+ seen := make(map[string]bool, len(local)+len(remote))
+ out := make([]string, 0, len(local)+len(remote))
+ add := func(d string) {
+ d = strings.ToLower(strings.TrimSpace(d))
+ if d == "" || seen[d] {
+ return
+ }
+ seen[d] = true
+ out = append(out, d)
+ }
+ for _, d := range local {
+ add(d)
+ }
+ nLocal := len(out)
+ for _, d := range remote {
+ add(d)
+ }
+ if len(out) > nLocal && zone == "" {
+ delegationZoneUnsetOnce.Do(func() {
+ log.Printf("edge: %d delegated wildcard parent(s) published by other boxes but CARLOS_ACME_DELEGATION_ZONE is unset here — set it fleet-wide, or this box cannot issue for them if it becomes the issuer", len(out)-nLocal)
+ })
+ }
+ if len(out) > maxDelegatedParents {
+ if n := int32(len(out)); delegatedCapLogged.Swap(n) != n {
+ log.Printf("edge: delegated wildcard parents capped at %d (%d distinct: %d local, %d remote) — the overflow gets no wildcard", maxDelegatedParents, len(out), nLocal, len(out)-nLocal)
+ }
+ out = out[:maxDelegatedParents]
+ }
+ return out
+}
+
+// Standing conditions are said once: an unset zone stays unset, and the cap
+// line repeats only when the count it reports changes.
+var (
+ delegationZoneUnsetOnce sync.Once
+ delegatedCapLogged atomic.Int32
+)
+
// parentsFor computes the wildcard parent set: staticAndDelegated (the
// static CARLOS_WILDCARD_DOMAINS plus any delegated parents refresh() has
// already vetted — both arrive pre-merged through this one parameter) union
@@ -804,7 +857,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 unionDelegated(out, e.homes.Load().WildcardParents(), zone)
}
out := make([]string, 0, len(wds))
for _, w := range wds {
@@ -813,7 +866,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 unionDelegated(out, e.homes.Load().WildcardParents(), zone)
}
// 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..ddc3f3c54 100644
--- a/internal/edge/wildcard_parents_test.go
+++ b/internal/edge/wildcard_parents_test.go
@@ -3,6 +3,8 @@ package edge
import (
"context"
"fmt"
+ "log"
+ "os"
"path/filepath"
"reflect"
"sort"
@@ -297,3 +299,164 @@ 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")
+ }
+}
+
+// Two boxes publishing the same "*.<parent>" key merge CLASSES and pick one
+// dial target (route beats alias, any beats none), whatever the sort order
+// — a delegated parent's PEM must load everywhere even when another box
+// routes the key too. A cert-only entry is a parent but never a dial target.
+func TestHomeMapWildcardCollisionMergesClasses(t *testing.T) {
+ for _, order := range [][2]string{{"aaa", "zzz"}, {"zzz", "aaa"}} {
+ routeBox, wildBox := order[0], order[1]
+ s := meshTestStore(t)
+ putJSON(t, s, "control/edges/"+routeBox+".json", `{"v":1,"box":"`+routeBox+`","eip":"1.1.1.1","hosts":{"*.customer.example":{"role":"home","platform":true}}}`)
+ putJSON(t, s, "control/edges/"+wildBox+".json", `{"v":1,"box":"`+wildBox+`","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"wildcard","wildcard":true},"*.certonly.example":{"role":"wildcard","wildcard":true}}}`)
+ m := newHomeMap(s, "self")
+ refreshHomeMap(t, m)
+ if _, box, ok := m.Home("x.customer.example"); !ok || box != routeBox {
+ t.Fatalf("order %v: dial = %s/%v; want the route's box %s", order, box, ok, routeBox)
+ }
+ if got := m.WildcardParents(); !reflect.DeepEqual(got, []string{"certonly.example", "customer.example"}) {
+ t.Fatalf("order %v: WildcardParents = %v", order, got)
+ }
+ if _, _, ok := m.Home("x.certonly.example"); ok {
+ t.Fatalf("order %v: a cert-only wildcard entry was a dial target", order)
+ }
+ }
+ // Role wildcard on an EXACT key is refused (no KnownHost admission
+ // without a dial); a file with no EIP still contributes cert halves.
+ s2 := meshTestStore(t)
+ putJSON(t, s2, "control/edges/carol.json", `{"v":1,"box":"carol","eip":"","hosts":{"*.noeip.example":{"role":"home","wildcard":true},"victim.example":{"role":"wildcard"},"served.example":{"role":"home","platform":true}}}`)
+ m2 := newHomeMap(s2, "self")
+ refreshHomeMap(t, m2)
+ if got := m2.WildcardParents(); !reflect.DeepEqual(got, []string{"noeip.example"}) {
+ t.Fatalf("EIP-less file: WildcardParents = %v", got)
+ }
+ if m2.KnownHost("victim.example") || m2.KnownHost("served.example") {
+ t.Fatal("an exact key from an EIP-less file or with role wildcard was admitted")
+ }
+ if _, _, ok := m2.Home("x.noeip.example"); ok {
+ t.Fatal("an EIP-less file offered a dial target")
+ }
+ // A remote parent of bad shape is not a parent.
+ s := meshTestStore(t)
+ putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.nodot":{"role":"wildcard","wildcard":true},"*.*.double.example":{"role":"wildcard","wildcard":true},"*.ok.example":{"role":"wildcard","wildcard":true}}}`)
+ m := newHomeMap(s, "self")
+ refreshHomeMap(t, m)
+ if got := m.WildcardParents(); !reflect.DeepEqual(got, []string{"ok.example"}) {
+ t.Fatalf("WildcardParents = %v", got)
+ }
+}
+
+// The fleet's delegated set is capped where the per-box table is: local
+// rows first, the overflow logged and dropped.
+func TestUnionDelegatedCapsAndWarns(t *testing.T) {
+ delegatedCapLogged.Store(0) // once-per-process throttles: isolate from -count>1
+ delegationZoneUnsetOnce = sync.Once{}
+ var buf strings.Builder
+ log.SetOutput(&buf)
+ t.Cleanup(func() { log.SetOutput(os.Stderr) })
+ local := []string{"l1.example", "l2.example"}
+ remote := make([]string, 0, 40)
+ for i := 0; i < 40; i++ {
+ remote = append(remote, fmt.Sprintf("r%02d.example", i))
+ }
+ remote = append(remote, "L1.example", "r00.example") // duplicates never cost a slot
+ got := unionDelegated(local, remote, "acme.apps.test")
+ if len(got) != maxDelegatedParents || got[0] != "l1.example" || got[1] != "l2.example" || got[2] != "r00.example" {
+ t.Fatalf("union = %d entries, head %v", len(got), got[:3])
+ }
+ if got := unionDelegated([]string{"a.example"}, []string{"A.example", "a.example", "b.example"}, "z"); !reflect.DeepEqual(got, []string{"a.example", "b.example"}) {
+ t.Fatalf("dedupe = %v", got)
+ }
+ if !strings.Contains(buf.String(), "capped at 32") {
+ t.Fatalf("overflow not logged:\n%s", buf.String())
+ }
+ buf.Reset()
+ if got := unionDelegated(local, []string{"r.example"}, ""); len(got) != 3 || !strings.Contains(buf.String(), "CARLOS_ACME_DELEGATION_ZONE is unset") {
+ t.Fatalf("no-zone case: %v / %s", got, buf.String())
+ }
+ buf.Reset()
+ unionDelegated(local, []string{"r.example"}, "") // a standing condition is said once
+ if buf.Len() != 0 {
+ t.Fatalf("zone-unset line repeated:\n%s", buf.String())
+ }
+ if got := unionDelegated(local, nil, ""); len(got) != 2 || buf.Len() != 0 {
+ t.Fatalf("local-only case logged or changed: %v / %s", got, buf.String())
+ }
+}
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go
index 715c7467d..b44827d49 100644
--- a/internal/host/edgesfile.go
+++ b/internal/host/edgesfile.go
@@ -49,6 +49,12 @@ 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>" and every edge should load the parent's
+ // mirrored wildcard PEM. With role "home" the box also serves the
+ // catch-all (dial target); with role "wildcard" it is the cert half
+ // alone. Additive (omitempty); never platform.
+ Wildcard bool `json:"wildcard,omitempty"`
}
// edgesFile is the wire shape at edgesFileKey(box), schema v1 (spec Part 3):
@@ -186,6 +192,55 @@ 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)
+ }
+ // Two halves, published separately: the CERT half (every edge loads the
+ // parent's PEM) always; the DIAL half (role home — remote edges backhaul
+ // one-label hosts under the parent) only when this box actually serves
+ // the catch-all, i.e. the claim has Catchall and its "*.<parent>" alias
+ // row points at a served primary. A Wildcard claim without Catchall
+ // serves only its exact aliases (published above, each on its own
+ // terms); advertising a dial target for it would have region edges
+ // terminate and backhaul any invented name under the parent to a box
+ // that answers 503.
+ catchallServed := map[string]bool{}
+ for _, a := range aliases {
+ if parent, ok := strings.CutPrefix(a.Alias, "*."); ok {
+ if _, served := hosts[a.Host]; served {
+ catchallServed[parent] = true
+ }
+ }
+ }
+ for _, wd := range wds {
+ if wd.Host == "" || strings.HasPrefix(wd.Host, "*.") {
+ continue
+ }
+ key := "*." + wd.Host
+ if prev, taken := hosts[key]; taken {
+ // A real route owns the key (its catch-all row, published above):
+ // the route keeps its role and class, and the parent's class is
+ // ADDED — the reader's fold merges classes the same way, and a
+ // parent whose PEM must load everywhere is a fact about the name,
+ // not about which row got there first.
+ prev.Wildcard = true
+ hosts[key] = prev
+ continue
+ }
+ role := "wildcard"
+ if catchallServed[wd.Host] {
+ role = "home"
+ }
+ hosts[key] = edgeHost{Role: role, 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..c19c1e219 100644
--- a/internal/host/edgesfile_test.go
+++ b/internal/host/edgesfile_test.go
@@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"strings"
+ "sync"
"testing"
"time"
@@ -620,6 +621,7 @@ func TestEdgesFileMarksPlatformRows(t *testing.T) {
// aliases, never platform-class, catch-alls never, and never over a real
// route. Detaching one republishes without it.
func TestEdgesFilePublishesPointingAliasesOfServedPrimaries(t *testing.T) {
+ withheldAliasSaid = sync.Map{} // once per PROCESS: isolate from -count>1
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)
@@ -695,3 +697,74 @@ 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)
+ }
+ // customer.example has a served catch-all (Catchall claim) → dial half
+ // too; certonly.example is a Wildcard claim without Catchall → cert
+ // half only; pending.example's catch-all points at an unserved primary.
+ if err := reg.Upsert(registry.Route{Host: "pend.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3002", Status: "pending"}); err != nil {
+ t.Fatal(err)
+ }
+ for _, a := range []registry.RouteAlias{
+ {Alias: "*.customer.example", Host: "app.acct.apps.test", Account: "acct", App: "app"},
+ {Alias: "*.pending.example", Host: "pend.acct.apps.test", Account: "acct", App: "pend"},
+ } {
+ if err := reg.UpsertAlias(a); err != nil {
+ t.Fatal(err)
+ }
+ }
+ for _, w := range []registry.WildcardDomain{
+ {Host: "customer.example", Account: "acct", App: "app"},
+ {Host: "certonly.example", Account: "acct", App: "app"},
+ {Host: "pending.example", Account: "acct", App: "pend"},
+ {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)
+ }
+ for _, name := range []string{"*.certonly.example", "*.pending.example"} {
+ h, ok := f.Hosts[name]
+ if !ok || h.Role != "wildcard" || !h.Wildcard || h.Platform {
+ t.Errorf("%s = %+v, %v; want the cert half only (role wildcard)", name, h, ok)
+ }
+ }
+ if _, ok := f.Hosts["*.*.odd.example"]; ok {
+ t.Error("a malformed wildcard_domains row was published")
+ }
+ // A real route owns the key: it keeps role and class, and the parent's
+ // class is added — the reader must still load the PEM everywhere.
+ if h := f.Hosts["*.taken.example"]; !h.Platform || !h.Wildcard || h.Role != "home" {
+ t.Errorf("route-owned key = %+v; want platform AND wildcard, role home", h)
+ }
+}