|
|
|
@@ -0,0 +1,147 @@
|
|
|
|
|
---
|
|
|
|
|
status: proposed
|
|
|
|
|
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. The same move was already made for
|
|
|
|
|
`wireguard`; this applies it to the other offender.
|
|
|
|
|
|
|
|
|
|
### 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 two remaining database credentials leaves the node.** `wireguard` was the other, and
|
|
|
|
|
it is already handled — so this closes the set that ADR 0039 identified.
|
|
|
|
|
- **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.
|
|
|
|
|
- **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.
|