Files
hq/02-DECISIONS/0049-a-route-is-a-grant.md
T
jschoubben ef5dd0751b Approve 0049-0053; drop a to-be item superseded by ADR 0044
The 'domain grouping' item cited ADR 0017 as live guidance. 0044 superseded
it -- there is no domain module to group into, so there is no domain list to
settle.
2026-08-27 01:00:29 +02:00

154 lines
8.2 KiB
Markdown

---
status: accepted
date: 2026-08-27
deciders: jochen
reconstructed: false
extends: 0044-a-module-declares-presence-instantiation-and-exclusion.md
---
# 49. A route is a grant
## Context
[ADR 0048](0048-the-substrate-is-named.md) named the substrate's products and, in doing so,
found a hole rather than a naming problem: the `connectivity` context lists *exposure* and
*certificates* among its responsibilities, and **no document says what terminates TLS, how a
public name reaches a container, or which tier owns any of it.**
Traefik is what does it today, and how it does it is the problem.
[Research 006](../01-RESEARCH/006-mesh-from-scratch/host-size.md) counted it as one of only two
modules that **open a direct Postgres connection to the control plane's database** — reading
`nodes` and `mesh_ca` and computing its own configuration from them.
That is three violations in one module:
- **[ADR 0037](0037-the-host-applies-it-does-not-decide.md)** — it decides, on the node, from
mesh-wide knowledge.
- **[ADR 0045](0045-a-context-owns-its-store.md)** — it reads another context's tables directly.
- **[ADR 0039](0039-the-link-is-the-security-boundary.md)** — it is the reason every node
permanently holds a credential to the control plane's database.
So exposure was never designed; it was accreted, and it is one of the two things standing
between the current arrangement and the security boundary ADR 0039 describes.
## Decision
### Ingress is not substrate
The test from [`07-the-substrate.md`](../03-DESIGN/01-to-be/07-the-substrate.md), applied
honestly:
| | |
|---|---|
| Does the control plane need a route to **start**? | **No.** It listens locally. |
| Does a **node** need one to reach it? | **No** — the node dials out over AMQP, and has no listening control surface at all ([ADR 0039](0039-the-link-is-the-security-boundary.md)). |
| Does anything need one **before the control plane runs**? | **No.** |
**So ingress is an ordinary provider module**, provisioned like anything else once a mesh exists.
The strongest objection deserves stating rather than dodging: the control plane's `api` is the
one interface every surface speaks to, so eventually it *does* want a public name. But *wanting
one later* is not *needing one to start* — that distinction is the entire substrate test, and
ingress is on the ordinary side of it. At bootstrap the first node's surface is reached locally,
and the mesh grants itself a route afterwards, the same way it grants itself a bucket.
### A route is an instantiation edge
A module that must be reachable declares it needs a **route**, and the proxy module provides
one. This is [ADR 0044](0044-a-module-declares-presence-instantiation-and-exclusion.md)'s
instantiation edge with no extension: a provider makes something for a consumer and hands back
an identity.
The direction is worth noticing, because it is the mirror of a database and could be mistaken
for a different kind of thing:
| | a database grant | a route grant |
|---|---|---|
| consumer supplies | nothing | where to send traffic |
| consumer receives | credentials | **the public name it is reachable at** |
Both are still *the provider made something and told the consumer how to use it*, which is what
the edge means. Nothing new is required.
### Exposure is three facts at two scopes, and that is why it is a context
The reason this cannot simply live on the node:
| The fact | Scope | Whose |
|---|---|---|
| the public name resolves to an address | **mesh** — which node is publicly reachable | the **control plane**, `connectivity` |
| a certificate valid for that name exists | **mesh** — issued once, for a name, used on one node | the **control plane**, `connectivity` |
| the proxy maps that name to that container | **node** | the **host**, applying a declaration |
Two of the three need to know about more than one node, which is exactly
[the control plane's definition](../03-DESIGN/01-to-be/06-the-control-plane.md). The third is a
single machine's business. The line falls where the tier rule already puts it, and the current
arrangement is wrong precisely because Traefik does all three on the node.
### The proxy reads files; it does not read the mesh
The connectivity context computes the proxy's configuration and the certificate, and they arrive
over the link as **`file` resources**.
**This costs zero new host vocabulary.** `file` already exists — it was one of the first three
shapes built. The proxy becomes a `container` with `file` configuration, which the host already
knows how to apply and read back.
And it removes a database credential from every node, which is half of what
[ADR 0039](0039-the-link-is-the-security-boundary.md) is for.
[ADR 0037](0037-the-host-applies-it-does-not-decide.md) decided this in principle; **neither
offender has actually been changed**, and this applies it to one of the two.
### A node without a public address is routed through one that has
Most nodes sit behind a connection with no forwarded port
([research 004](../01-RESEARCH/004-lab-network/00-overview.md)), so exposure cannot assume the
workload's node is reachable. Two cases, and the mesh must handle both because the difference is
invisible until it matters:
- **the node is publicly reachable** — the proxy runs there and the route is direct;
- **it is not** — a publicly reachable node proxies to it across the overlay.
Which case applies is a mesh-level fact, which is the fourth reason exposure is control-plane
work. Research 004 already records the hard variant — *a node that is publicly named but sits
behind NAT* — as the case the lab exists to get right.
## Consequences
- **Traefik's upward dependency is removed, and the module mostly disappears.** It computed its
own configuration; now it is an image plus files somebody else derived. Of the 426 lines
research 006 counted, what survives is a declaration.
- **One of the two direct database connections goes.** `wireguard` is the other and is *not*
yet handled — [ADR 0050](0050-reachability-is-a-property-of-the-address.md) is what handles it,
and only both together close the set ADR 0039 identified. Until then a node still holds the
credential, so this record alone changes the design and not the exposure.
- **Certificate issuance becomes a control-plane responsibility with a real constraint**: an
ACME challenge can only be answered at a publicly reachable address, so issuance happens
through a public node regardless of where the workload runs. The lab already runs its own
issuer, so this is testable ([research 004](../01-RESEARCH/004-lab-network/00-overview.md)).
- **The proxy is a presence edge for anything exposed.** A module with a route needs the proxy on
its node — ordinary ADR 0044 vocabulary, no special case.
- **This does not settle the mesh's internal CA.** `mesh_ca` is the *other* thing Traefik reads,
and it belongs to a different question: ADR 0039 requires the control plane to prove it is the
mesh, which needs something a joining node can verify before it trusts anything. **Internal
identity and public exposure are two certificate stories and this record only closes the
second.** Conflating them is what made the gap hard to see.
**Since closed** by [ADR 0051](0051-the-enrolment-token-carries-the-mesh.md): the joining node
verifies the control plane against a fingerprint carried in its enrolment token, so nothing
needs the CA before membership.
- **Nothing says how a route is revoked** when a module is unassigned. The grant model implies
it — removing a consumer drops what it was granted
([ADR 0045](0045-a-context-owns-its-store.md)) — but a stale public name pointing at nothing is
a more visible failure than a stale database, and it is not designed.
## References
- [ADR 0044](0044-a-module-declares-presence-instantiation-and-exclusion.md) — the edge a route is.
- [ADR 0037](0037-the-host-applies-it-does-not-decide.md) — why the proxy may not decide.
- [ADR 0039](0039-the-link-is-the-security-boundary.md) — the credential this removes.
- [ADR 0048](0048-the-substrate-is-named.md) — which named this gap and left it open.
- [Research 006](../01-RESEARCH/006-mesh-from-scratch/host-size.md) — the count and the two
offenders.
- [Research 004](../01-RESEARCH/004-lab-network/00-overview.md) — the topology and the lab's
own issuer.