edge: wildcard parents come from provisioned routes too, not only console-minted ones
A box whose routes are ALL records-provisioned — every multi-region edge, and any new account on a single box — derived no wildcard parents: currentHosts admitted only Platform-flagged rows, and the provisioning reconciler mints its rows with Platform=false by design (the flag marks the console's own pass). So a region edge with review.bdj.oncarlos.com adopted could not serve *.bdj.oncarlos.com even with the wildcard PEM mirrored into its cache — every handshake fell through to the gated autocert and failed 'acme: not available here'. Found on the first three-region flagship roll (2026-08-24). Provisioned rows now count as platform-minted for parent derivation. A provisioned CUSTOM domain is harmless: parentsFor mints parents only for hosts exactly two labels under the apps domain, pinned in the test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+41
−19
internal/edge/edge.go+17 −9internal/edge/wildcard_test.go+24 −10
diff --git a/internal/edge/edge.go b/internal/edge/edge.go| index db04072b0..7b376d8ff 100644 |
| --- a/internal/edge/edge.go |
| +++ b/internal/edge/edge.go |
| @@ -695,15 +695,23 @@ func (e *Edge) current() map[string]upstream { |
| return nil |
| } |
| -// currentHosts lists the hosts of PLATFORM-minted routes only |
| -// (upstream.route.Platform == true) — the wildcard manager's view of the |
| -// route table for parent derivation. Not every served host: parents are |
| -// derived from these hosts, and if a custom domain shaped like |
| -// x.y.<apps-domain> ever entered the route table, it would mint a wildcard |
| +// currentHosts lists the hosts of platform-minted routes — rows the console |
| +// minted directly (Platform) or the provisioning reconciler minted from a |
| +// bucket record (Provisioned) — the wildcard manager's view of the route |
| +// table for parent derivation. Not every served host: parents are derived |
| +// from these hosts, and if a custom domain shaped like x.y.<apps-domain> |
| +// ever entered the route table by some other path, it would mint a wildcard |
| // parent and burn a Let's Encrypt issuance on something the wildcard was |
| -// never meant to cover. Platform-minted app routes set Platform:true; static |
| -// parents (e.g. tito's go.tito.io) come from CARLOS_WILDCARD_DOMAINS via env, |
| -// so they are unaffected by this filter. |
| +// never meant to cover. Provisioned rows are admitted since 2026-08-24: a |
| +// box whose routes are ALL records-provisioned (every multi-region edge, and |
| +// any new account on a single box) derived no parents at all and could not |
| +// serve a single mirrored wildcard — found on the first three-region |
| +// flagship roll. A provisioned CUSTOM domain (a static record like |
| +// story.carlosframework.com) is harmless here: parentsFor only mints a |
| +// parent for hosts exactly two labels under the apps domain, and custom |
| +// domains under the apps domain are refused at the console. Static parents |
| +// (e.g. tito's go.tito.io) come from CARLOS_WILDCARD_DOMAINS via env, so |
| +// they are unaffected by this filter. |
| // |
| // Attached hostnames (#151) are excluded by the !up.alias half: an alias |
| // entry carries the PRIMARY's route row verbatim, Platform flag included, so |
| @@ -713,7 +721,7 @@ func (e *Edge) currentHosts() []string { |
| m := e.current() |
| out := make([]string, 0, len(m)) |
| for h, up := range m { |
| - if up.route.Platform && !up.alias { |
| + if (up.route.Platform || up.route.Provisioned) && !up.alias { |
| out = append(out, h) |
| } |
| } |
diff --git a/internal/edge/wildcard_test.go b/internal/edge/wildcard_test.go| index 17b90d41d..8b3f5e182 100644 |
| --- a/internal/edge/wildcard_test.go |
| +++ b/internal/edge/wildcard_test.go |
| @@ -12,6 +12,7 @@ import ( |
| "math/big" |
| "reflect" |
| "slices" |
| + "sort" |
| "strings" |
| "testing" |
| "time" |
| @@ -408,24 +409,37 @@ func TestOnReloadHookFires(t *testing.T) { |
| } |
| // TestCurrentHostsPlatformOnly guards the wildcard-safety filter: only |
| -// PLATFORM-minted routes (Platform==true) contribute to currentHosts, since |
| -// parentsFor derives wildcard parents from that list. A custom domain |
| -// shaped like <x>.<y>.<apps-domain> that somehow landed in the route table |
| -// (Platform==false) must never mint a wildcard parent and burn an ACME |
| -// issuance for it — only the routes the domains-reconcile pass itself |
| -// minted are trusted for that. |
| +// platform-minted routes contribute to currentHosts — console-minted |
| +// (Platform==true) or records-provisioned (Provisioned==true) — since |
| +// parentsFor derives wildcard parents from that list. A host shaped like |
| +// <x>.<y>.<apps-domain> that landed in the route table by neither path |
| +// must never mint a wildcard parent and burn an ACME issuance for it. |
| +// |
| +// Provisioned rows joined the set on 2026-08-24: a box holding ONLY |
| +// provisioned rows (every multi-region edge) derived no parents and served |
| +// no mirrored wildcard. A provisioned custom domain is listed here but |
| +// mints nothing — parentsFor's two-labels-under-the-apps-domain rule is the |
| +// bound, pinned below. |
| func TestCurrentHostsPlatformOnly(t *testing.T) { |
| e := &Edge{} |
| e.upstreams.Store(&map[string]upstream{ |
| - "app.acct.oncarlos.com": {route: registry.Route{Host: "app.acct.oncarlos.com", Platform: true}}, |
| - "custom.example.com": {route: registry.Route{Host: "custom.example.com", Platform: false}}, |
| - "evil.acct2.oncarlos.com": {route: registry.Route{Host: "evil.acct2.oncarlos.com", Platform: false}}, |
| + "app.acct.oncarlos.com": {route: registry.Route{Host: "app.acct.oncarlos.com", Platform: true}}, |
| + "review.acct3.oncarlos.com": {route: registry.Route{Host: "review.acct3.oncarlos.com", Provisioned: true}}, |
| + "story.example.com": {route: registry.Route{Host: "story.example.com", Provisioned: true}}, |
| + "custom.example.com": {route: registry.Route{Host: "custom.example.com", Platform: false}}, |
| + "evil.acct2.oncarlos.com": {route: registry.Route{Host: "evil.acct2.oncarlos.com", Platform: false}}, |
| }) |
| got := e.currentHosts() |
| - want := []string{"app.acct.oncarlos.com"} |
| + sort.Strings(got) |
| + want := []string{"app.acct.oncarlos.com", "review.acct3.oncarlos.com", "story.example.com"} |
| if !reflect.DeepEqual(got, want) { |
| t.Fatalf("currentHosts = %v, want %v", got, want) |
| } |
| + parents := parentsFor(nil, "oncarlos.com", got) |
| + wantParents := []string{"acct.oncarlos.com", "acct3.oncarlos.com"} |
| + if !reflect.DeepEqual(parents, wantParents) { |
| + t.Fatalf("parents = %v, want %v (a provisioned custom domain mints no parent; the unowned evil row never reaches here)", parents, wantParents) |
| + } |
| } |
| func TestDelegatedParentsJoinTheSet(t *testing.T) { |