ADR 0056 + connectivity: public routing is name-agnostic, resolved in-mesh, internally certifiable

A route contribution carries a label; the node carries its public domain; the
mesh composes <label>.<public-domain> and holds no name map. A granted route is
published into internal resolution so anything in-mesh (notably an internal ACME
authority) can resolve and reach it. That authority certifies routed names by the
same path a public one would, differing only in issuer and trusted root.

Records the decision as proposed and amends connectivity SS2/SS3/SS5 plus its Open
list with the lab findings behind it.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-09 22:24:43 +02:00
parent 9dfa5ba2ac
commit e2afb3e144
2 changed files with 186 additions and 1 deletions
+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.