Genesis is a pivot, public routing is name-agnostic, and five issues the fake registry was hiding #32

Merged
jschoubben merged 19 commits from design/bootstrap-is-a-pivot into main 2026-09-11 19:56:50 +00:00
2 changed files with 186 additions and 1 deletions
Showing only changes of commit e2afb3e144 - Show all commits
@@ -0,0 +1,115 @@
---
topic: routing and names
status: proposed
date: 2026-09-09
deciders: jochen
reconstructed: false
extends: 0007-connectivity.md
---
# 56. Public routing is name-agnostic, its names are resolved inside the mesh, and an internal authority can certify them
## Context
**[ADR 0007](0007-connectivity.md) and [connectivity §3](../03-DESIGN/01-to-be/08-connectivity.md)
made a public route a grant: a workload that must be reachable requires a route, the proxy provides
it, the consumer contributes the name it wants and the port it listens on.** What was never pinned
is **what that name is** — and building a whole mesh in the lab showed the gap costs more than it
looks.
**The catalogue shipped each route as a full domain.** A module that needed a public name carried
that name, in full, as a literal in its manifest. Running the same catalogue against a different
domain — a lab standing in for production, or a second operator's mesh — meant overriding that
literal on every routed module, per node. The mesh was, in effect, carrying a **map of names to
services**: the one thing it should never hold, because a name is the operator's choice (one runs
the forge at `git`, another at `code`) and the domain is the node's, and neither is the mesh's to
know.
**And a second gap surfaced the moment an internal issuer tried to certify those names.**
[Connectivity §5](../03-DESIGN/01-to-be/08-connectivity.md) already states the issuer must be
configurable and that the lab runs its own ACME authority. With that authority wired to the proxy,
issuance still could not complete: the authority accepted the order and offered a challenge, then
**could not connect to the validation target.** Nothing inside the mesh resolved the public route
name. The mesh publishes each `<node>.internal` name into every container, but not the public names
the proxy serves — so a validator living in the mesh had no address to reach, and a name the mesh
cannot resolve is a name it cannot have certified.
**The two are one problem.** A name the mesh can *compose* from parts it is given, and *propagate*
to whoever needs to resolve it, is exactly a name it can also have *certified* — and the reverse:
without the composition and the propagation, neither the routing nor the certificate is the
operator's to move between meshes.
## Considered Options
**1. Keep the full domain in the manifest, override per node.** The status quo. It works, and it is
wrong in the specific way this repository cares about: the catalogue holds a domain map, lab and
production differ by an override on every routed module rather than one fact, and a module manifest
names something — the public domain — that belongs to the node, not the module. An unowned name in
the wrong place is the shape of a leak.
**2. A module declares a label; the node declares its public domain; the mesh composes.** The route
contribution carries a subdomain the operator chose, the node carries its public domain as
node-level configuration, and the mesh joins `<label>.<public-domain>` and grants exactly that. The
mesh interprets nothing. Lab-versus-production becomes one node setting. Chosen.
**3. For certification, issue only from a publicly reachable node against a public authority.**
This is already true for public meshes and stays true. It is not an option for a lab or an
internal-only mesh: there is no public authority to answer, and no public reachability to validate
against. An internal issuer is required there — and an internal issuer must be able to *validate*,
which it cannot do unless the routed name resolves and is reachable **inside** the mesh. So the
resolution gap is not optional to close; it is what makes an internal authority possible at all.
## Decision
**The mesh core holds no map of hostnames, subdomains or domains.** A route contribution carries a
**label** (the subdomain) chosen by the module's operator. A node contributes its **public domain**
as node-level configuration. The mesh composes `<label>.<public-domain>`, grants exactly that name,
and never interprets what it means. One operator's forge at `git.example.tld` and another's at
`code.other.example` are the same module with two facts supplied around it.
**When the proxy is granted a name, the mesh publishes that name → the node that serves it into
internal resolution, mesh-wide** — the same mechanism, and the same "given by the mesh, not chosen
by a module," that already writes `<node>.internal` into every declared container
([connectivity §2](../03-DESIGN/01-to-be/08-connectivity.md)). The mesh propagates the names it was
told to serve. It still knows nothing about what any of them mean.
**An internal authority certifies those names by the same path a public one would.** The proxy is
pointed at whichever issuer the mesh names — a public ACME authority, or an internal one — and
trusts that issuer's root; nothing else about issuance changes. The internal authority validates by
reaching the routed name, which the clause above has just made resolvable inside the mesh. So the
three are one decision: **compose the name, propagate it, certify it** — each is meaningless without
the one before it.
## Consequences
- **Lab-versus-production is one node-level `public-domain` setting**, not an override on every
routed module. The same catalogue runs against any domain.
- **The manifest layer needs composition it does not yet have.** Today a route name is stored as a
literal, with no interpolation of a node's domain into a module's label. Until that exists, the
composed name is produced by a per-node settings override — a stopgap that reproduces option 1's
per-module cost and is explicitly *not* the design.
- **An internal issuer depends on route-name resolution.** Its challenge validates against the
routed name; without that name in internal resolution, issuance for it cannot complete inside the
mesh. The lab found this as a live failure, not a theory.
- **Nothing about the public path changes.** A publicly reachable node issuing a public name from a
public authority is untouched; this widens the same shape to names and meshes that are not public.
**How each is checked** — an unenforced rule is indistinguishable from a wrong one:
- **Name-agnostic:** the same catalogue resolves against two different public domains by changing
one node setting and nothing else; and no module manifest contains a full public domain. A
manifest that pins an FQDN is the smell the check looks for.
- **Resolution:** a request to a routed name, made from inside the mesh, reaches the workload that
serves it — and, the sharper check, issuance for that name against the internal authority
completes, which it cannot unless the validator resolved and reached the target.
- **Internal authority:** a TLS handshake to a routed name verifies against the internal root and
nothing else, the same shape §5 already uses for internal node-to-node names.
## References
- [ADR 0007 — connectivity](0007-connectivity.md), which made a route a grant and named exposure,
resolution and certificates as one context.
- [ADR 0009 — modules and the graph](0009-modules-and-the-graph.md), the provide/require/contribute
vocabulary a route and an authority both use.
- [Connectivity design §2, §3, §5](../03-DESIGN/01-to-be/08-connectivity.md), amended alongside this
record.
+71 -1
View File
@@ -7,7 +7,7 @@ code:
- mesh-control internal/identity/authority.go
- mesh-host internal/identity/serving.go
- mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-08-31
updated: 2026-09-09
decisions:
- 02-DECISIONS/0005-the-node-host.md
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
@@ -16,6 +16,7 @@ decisions:
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
- 02-DECISIONS/0007-connectivity.md
- 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
- 02-DECISIONS/0056-public-routing-is-name-agnostic.md
---
# Connectivity
@@ -343,6 +344,27 @@ that module and nothing else.
the argument for the table in ADR 0009 being a table: the pattern is only obvious once seen, and
the cost of not seeing it is inventing a mechanism that already exists.
### And the public names a proxy serves must resolve in the mesh too
*2026-09-09, found by an internal certificate authority that could not issue.* The mesh writes every
`<node>.internal` name into every declared container and treats the public names a proxy serves as a
separate matter — *what routes it once it arrives is a proxy's, and stays separate*, above. That
holds for a client dialling by internal name. It does not hold for anything **inside** the mesh that
must reach a public name, and the first such thing to appear was the internal issuer of §5.
**An issuer validates by connecting to the name it is certifying.** Asked for a certificate for a
routed public name, the internal authority accepted the order, offered a challenge, and then could
not connect: nothing in the mesh resolved that name, so the challenge had no target. A name the mesh
can reach from the outside but cannot resolve from the inside is a name it cannot certify with an
authority of its own.
**So a granted route is published into internal resolution as well** — the routed name to the node
that serves it, mesh-wide, by the same mechanism that writes the node names. It is *given by the
mesh, not chosen by a module*, for the same reason the node names are: a module listing the routes
would go stale the day one changes. The mesh propagates the names it was told to serve and still
knows nothing about what they mean
([ADR 0056](../../02-DECISIONS/0056-public-routing-is-name-agnostic.md)).
## 3 — Exposure
Settled by [ADR 0007](../../02-DECISIONS/0007-connectivity.md); summarised here because
@@ -389,6 +411,27 @@ the mesh to tell them apart.
returning the workload's own answer, then by unassigning the module and requiring the same request
to stop working.*
### The name is a label, not a domain
*2026-09-09, from running the whole mesh in the lab.* A route contribution carried its public name
in full — the forge as `git.example.tld`, spelt out in the module. Pointing the same catalogue at a
different domain — a lab standing in for production, or a second operator's mesh — meant rewriting
that name on every routed module. The mesh was holding a **map of names to services**, which is the
one thing it must not: a public name is two facts owned by two different places, and neither is the
module's manifest.
**A module contributes a label; the node contributes its public domain; the mesh composes.** The
operator chooses where the forge lives — `git`, or `code` — and that is the module's to say. The
domain is the node's, set once. The mesh joins them and grants `<label>.<public-domain>`,
interpreting neither half. Moving a mesh to another domain is one node setting, not an edit per
module.
**Today the name is still a literal, and that is the gap.** There is no interpolation of a node's
domain into a module's label, so the composition is done by a per-node override — which reproduces
exactly the per-module cost it is meant to remove. The design is the composition; the override is a
stopgap until the manifest layer can carry a label and a domain separately
([ADR 0056](../../02-DECISIONS/0056-public-routing-is-name-agnostic.md)).
## 4 — Filtering
**Derived from what is assigned here, and from the overlay's shape** — a node's open ports are a
@@ -539,6 +582,24 @@ generated, the other verifies against the mesh's authority and nothing else. Eve
passed while the server could not start — the key was present, the certificate was valid, and
nothing read either the way a server would.*
### An internal issuer, pointed at and trusted
*2026-09-09, from wiring one to the proxy in the lab.* §5 above asks for two things this build
leaned on: the issuer must be configurable, and the lab runs its own ACME authority rather than
collapsing the split. Wiring the proxy to that authority is the whole of it — the proxy is told
**which** issuer to use and given that issuer's **root** to trust, and every other step of issuance
is unchanged. **The same code path certifies against an internal authority as against a public one;
only the issuer differs.** That is what makes trusted certificates possible for a mesh whose names
the public internet cannot resolve.
**And it does not work until the routed name resolves inside the mesh** — the §2 finding above,
arriving here because this is what needed it. The authority's challenge reaches the routed name only
once that name is in internal resolution; a public authority is handed that dependency by public
DNS, and an internal one has to be handed it by the mesh. *Checked by a handshake to a routed name
that verifies against the internal root and nothing else — which cannot succeed unless the issuer
first reached the name to certify it*
([ADR 0056](../../02-DECISIONS/0056-public-routing-is-name-agnostic.md)).
## What this removes
The list is worth having in one place, because it is most of the argument:
@@ -572,3 +633,12 @@ The list is worth having in one place, because it is most of the argument:
it expressible; nothing here says the overlay or the resolver handle it.
- **Reporting declared-versus-observed.** ADR 0007 makes the disagreement detectable and does not
say who looks or what they are told.
- **Composing a route name from a label and a node's domain.**
[ADR 0056](../../02-DECISIONS/0056-public-routing-is-name-agnostic.md) makes the public name the
operator's to move between meshes, but the manifest layer still stores it as a literal — so today
the composition is a per-node override rather than the design. The interpolation that would let a
module carry a label and a node carry the domain, and the mesh join them, does not yet exist.
- **Publishing route names into internal resolution.** The same ADR requires a granted route to be
resolvable inside the mesh, not only routable from outside it; the mechanism that writes
`<node>.internal` into containers does not yet also write the routed names, which is why an
internal issuer cannot currently validate one without a hand-placed entry.