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:
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user