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:
2026-08-27 00:22:52 +02:00
parent 4d19e93900
commit 8d9282d86b
4 changed files with 162 additions and 10 deletions
+5 -7
View File
@@ -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