edge: KnownHost — exact membership of the mirrored home map
The three cert-adjacent gates need to ask "does some box in this deployment home this name, spelled exactly?" (#277). Home() cannot answer it: its one-label wildcard fallback is what makes an invented subdomain under a remote catch-all dialable, and a cert gate that admitted such a label would let it drive an ACME order and spend a read-through budget — PR #180's rule, verbatim. So KnownHost reads the same fold and refuses both the fallback and the "*." key itself, inheriting the fold's own-box and no-EIP exclusions (both documented, both unreachable-for-validation). Nil-receiver-safe, like Home. Unused until the gates are widened in the next commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+97
−0
internal/edge/homemap.go+37 −0internal/edge/homemap_test.go+60 −0
diff --git a/internal/edge/homemap.go b/internal/edge/homemap.go| index 044709a88..841d4e92a 100644 |
| --- a/internal/edge/homemap.go |
| +++ b/internal/edge/homemap.go |
| @@ -413,6 +413,43 @@ func (m *homeMap) Home(host string) (eip, box string, ok bool) { |
| return "", "", false |
| } |
| +// KnownHost reports whether host is an EXACT key in the mirrored home map — a |
| +// name some OTHER box in this deployment homes, spelled precisely. |
| +// |
| +// Deliberately NOT Home()'s one-label wildcard fallback, and refusing "*." |
| +// outright: this predicate widens cert issuance and challenge admission |
| +// (certsync.go's three gates, through Edge.deploymentHost), where PR #180's |
| +// rule stands — attacker-chosen labels under a catch-all must never drive an |
| +// ACME order or a bucket read, and their TLS coverage is the wildcard cert's |
| +// job (Allowed's own doc says the same thing for this box's own rows). The |
| +// widening loses nothing by staying exact: an order only ever exists for a |
| +// name that passed the exact gate, so a challenge only ever needs answering |
| +// for an exact name. |
| +// |
| +// Unlike Home it needs no EIP and no dial target — membership is the whole |
| +// question — but it reads the same fold, so it inherits both of the fold's |
| +// exclusions. Own-box hosts are excluded and that is harmless: they pass |
| +// Allowed on the other half of deploymentHost. Boxes with EIP=="" are |
| +// excluded and that is accepted: a box with no public IP offers no backhaul |
| +// target and appears in no DNS answer, so a validation for its hosts has |
| +// nowhere legitimate to land anyway. |
| +// |
| +// There is deliberately no m.self gate, unlike Home: Home fail-closes without |
| +// a self label because it could otherwise offer this box itself as a dial |
| +// target, and membership carries no such hazard — an unlabelled box's fold |
| +// keeps its own file, whose hosts are its own registry rows and so pass |
| +// Allowed regardless. |
| +// |
| +// Nil-receiver-safe, like Home: the map may not be armed yet (or at all, on a |
| +// box with no deployment store). |
| +func (m *homeMap) KnownHost(host string) bool { |
| + if m == nil || strings.HasPrefix(host, "*.") { |
| + return false |
| + } |
| + _, ok := m.snapshot().hosts[host] |
| + return ok |
| +} |
| + |
| // refreshKeys re-reads the mesh key pair. loadMeshKeys is already tolerant of |
| // a malformed .next and fail-closed on the current key (mesh.go); this only |
| // adds the mirror's own posture on top — the pair is replaced ONLY on a clean |
diff --git a/internal/edge/homemap_test.go b/internal/edge/homemap_test.go| index aa87a7df5..ce605928c 100644 |
| --- a/internal/edge/homemap_test.go |
| +++ b/internal/edge/homemap_test.go |
| @@ -235,11 +235,71 @@ func TestNilHomeMapIsInert(t *testing.T) { |
| if _, _, ok := m.Home("b.test"); ok { |
| t.Fatal("a nil home map must offer nothing") |
| } |
| + if m.KnownHost("b.test") { |
| + t.Fatal("a nil home map knows no host") |
| + } |
| if cur, next := m.meshKeys(); cur != nil || next != nil { |
| t.Fatal("a nil home map holds no mesh keys") |
| } |
| } |
| +// KnownHost is the membership question the three cert gates ask through |
| +// Edge.deploymentHost (#277), and it is EXACT-only where Home is not. Home's |
| +// one-label fallback is what makes an invented subdomain under a remote |
| +// catch-all DIALABLE; letting the same fallback into a cert gate would let |
| +// that invented label drive an ACME order and spend a read-through budget, |
| +// which is precisely what PR #180's Allowed rule refuses. The two answers |
| +// diverging on the same map, in the same test, is the pin. |
| +func TestKnownHostExactOnly(t *testing.T) { |
| + s := meshTestStore(t) |
| + putJSON(t, s, "control/edges/annie.json", |
| + `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"own.test":{"role":"home"}}}`) |
| + putJSON(t, s, "control/edges/betty.json", |
| + `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.apps.test":{"role":"home"},"exact.apps.test":{"role":"home"}}}`) |
| + m := newHomeMap(s, "annie") |
| + refreshHomeMap(t, m) |
| + |
| + if !m.KnownHost("exact.apps.test") { |
| + t.Fatal("an exactly-named remote-homed host is the whole point of the predicate") |
| + } |
| + // Precondition: the fallback Home applies is live on this very map — so |
| + // the refusal below is KnownHost's own, not an empty map's. |
| + if _, _, ok := m.Home("invented.apps.test"); !ok { |
| + t.Fatal("test bug: the one-label wildcard rule did not apply") |
| + } |
| + if m.KnownHost("invented.apps.test") { |
| + t.Fatal("a label under a mirrored catch-all must never be admitted — that is #180's rule") |
| + } |
| + if m.KnownHost("*.apps.test") { |
| + t.Fatal("the catch-all key itself is not a certifiable name") |
| + } |
| + if m.KnownHost("stranger.example") { |
| + t.Fatal("a host no box in the deployment homes is a stranger") |
| + } |
| + // Own-box rows are excluded by the fold and stay excluded here. Harmless: |
| + // deploymentHost's other half (Allowed) is what admits this box's routes. |
| + if m.KnownHost("own.test") { |
| + t.Fatal("the fold excludes this box's own file, so KnownHost inherits that exclusion") |
| + } |
| + // Nothing polled yet: no membership, and no panic on the gate's path. |
| + if newHomeMap(s, "annie").KnownHost("exact.apps.test") { |
| + t.Fatal("a map that has never polled knows no host") |
| + } |
| +} |
| + |
| +// A box publishing eip:"" is in the fold's no-dial exclusion, and KnownHost |
| +// inherits it: there is no DNS answer pointing at such a box, so a validation |
| +// for its hosts has nowhere legitimate to land. |
| +func TestKnownHostSkipsBoxesWithNoEIP(t *testing.T) { |
| + s := meshTestStore(t) |
| + putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"","hosts":{"b.test":{"role":"home"}}}`) |
| + m := newHomeMap(s, "annie") |
| + refreshHomeMap(t, m) |
| + if m.KnownHost("b.test") { |
| + t.Fatal("a box with no public IP offers no validation target") |
| + } |
| +} |
| + |
| // ---- hop acceptance --------------------------------------------------------- |
| // forwardEcho is an upstream that reports back exactly the trust-bearing |