Resolve the ingress gap: a route is a grant
ADR 0048 named ingress as an unclosed hole -- nothing said what terminates TLS, how a public name reaches a container, or which tier owned it. Resolving it needed no new concepts, which is why it survived: nobody had applied the rules already written to it. Ingress is not substrate. The control plane does not need a route to start, and no node needs one to reach it -- the node dials out and has no listening control surface. It grants itself a route afterwards, like a bucket. A route is an instantiation edge under ADR 0044. The direction mirrors a database -- the consumer supplies a target and receives a name rather than credentials -- but it is the same edge. The substantive finding is that exposure is three facts at two scopes: name resolution and certificate issuance need to know which node is publicly reachable, and only the proxy mapping is a single machine's business. That is why it belongs to the connectivity context, and why Traefik doing all three on the node is wrong. Which matters beyond tidiness: research 006 counted traefik as one of two modules opening a direct Postgres connection, reading nodes and mesh_ca. That violates 0037, 0045 and 0039 at once, and is why every node permanently holds a credential to the control plane's database. Deriving the config centrally and delivering it as `file` resources removes it, costs zero new host vocabulary, and closes the set 0039 identified -- wireguard was the other. Left open deliberately: the mesh's internal CA is the other thing traefik reads, and it belongs to the link's mutual authority, not to exposure. Conflating the two is what made the gap hard to see. Also fixes an inconsistency from the previous commit: 06 still claimed the virtual host was raised from the bundle. Proposed, not accepted -- for review.
This commit is contained in:
@@ -51,14 +51,12 @@ correction applies — a role is a legitimate abstraction, but the product belon
|
||||
|---|---|---|
|
||||
| **the forge** | **Gitea** | a hosted workload — the mesh builds from it but does not need it to run |
|
||||
| **the coordinator** | the mesh's own pipeline | tier 2 — part of the control plane, not a product |
|
||||
| **ingress** — *exposure*, *certificates* | **Traefik** | see below |
|
||||
| **ingress** — *exposure*, *certificates* | **Traefik** | not substrate — [ADR 0049](0049-a-route-is-a-grant.md) |
|
||||
|
||||
**Ingress is a real gap rather than a naming one, and this record does not close it.** The
|
||||
connectivity context lists *exposure* and *certificates* among its responsibilities, and no
|
||||
design document says what terminates TLS, how a route reaches a container, or which tier that
|
||||
belongs to. Traefik is what does it today. Whether it is substrate turns on the same test — can
|
||||
the control plane grant itself a route? — and nobody has applied the test. **Named here so the
|
||||
gap is visible; left open because naming it is not answering it.**
|
||||
**Ingress was a real gap rather than a naming one**, and it is closed by
|
||||
[ADR 0049](0049-a-route-is-a-grant.md): applying the same test shows it is **not** substrate, and
|
||||
a route is an ordinary grant. Recorded here because finding it was the point — naming the
|
||||
products is what made the unnamed role visible.
|
||||
|
||||
**Identity is deliberately absent.** Whether an identity provider is substrate at all depends on
|
||||
whether the control plane delegates authentication, which is undecided
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user