Merge branch 'feat/routing-is-name-agnostic' into design/bootstrap-is-a-pivot
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
|
||||||
|
---
|
||||||
|
|
||||||
|
# 66. 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-control internal/identity/authority.go
|
||||||
- mesh-host internal/identity/serving.go
|
- mesh-host internal/identity/serving.go
|
||||||
- mesh-host internal/apply (the service that reflects a rule set)
|
- mesh-host internal/apply (the service that reflects a rule set)
|
||||||
updated: 2026-08-31
|
updated: 2026-09-09
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0005-the-node-host.md
|
- 02-DECISIONS/0005-the-node-host.md
|
||||||
- 02-DECISIONS/0004-a-node-and-how-it-joins.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/0004-a-node-and-how-it-joins.md
|
||||||
- 02-DECISIONS/0007-connectivity.md
|
- 02-DECISIONS/0007-connectivity.md
|
||||||
- 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
|
- 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
|
||||||
|
- 02-DECISIONS/0066-public-routing-is-name-agnostic.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# Connectivity
|
# 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 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.
|
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 0066](../../02-DECISIONS/0066-public-routing-is-name-agnostic.md)).
|
||||||
|
|
||||||
## 3 — Exposure
|
## 3 — Exposure
|
||||||
|
|
||||||
Settled by [ADR 0007](../../02-DECISIONS/0007-connectivity.md); summarised here because
|
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
|
returning the workload's own answer, then by unassigning the module and requiring the same request
|
||||||
to stop working.*
|
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 0066](../../02-DECISIONS/0066-public-routing-is-name-agnostic.md)).
|
||||||
|
|
||||||
## 4 — Filtering
|
## 4 — Filtering
|
||||||
|
|
||||||
**Derived from what is assigned here, and from the overlay's shape** — a node's open ports are a
|
**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
|
passed while the server could not start — the key was present, the certificate was valid, and
|
||||||
nothing read either the way a server would.*
|
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 0066](../../02-DECISIONS/0066-public-routing-is-name-agnostic.md)).
|
||||||
|
|
||||||
## What this removes
|
## What this removes
|
||||||
|
|
||||||
The list is worth having in one place, because it is most of the argument:
|
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.
|
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
|
- **Reporting declared-versus-observed.** ADR 0007 makes the disagreement detectable and does not
|
||||||
say who looks or what they are told.
|
say who looks or what they are told.
|
||||||
|
- **Composing a route name from a label and a node's domain.**
|
||||||
|
[ADR 0066](../../02-DECISIONS/0066-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