Files
hq/02-DECISIONS/0049-a-route-is-a-grant.md
T
jschoubben 8d9282d86b 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.
2026-08-27 00:22:52 +02:00

7.7 KiB

status, date, deciders, reconstructed, extends
status date deciders reconstructed extends
proposed 2026-08-27 jochen false 0044-a-module-declares-presence-instantiation-and-exclusion.md

49. A route is a grant

Context

ADR 0048 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 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 — it decides, on the node, from mesh-wide knowledge.
  • ADR 0045 — it reads another context's tables directly.
  • ADR 0039 — 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, 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).
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'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. 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 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), 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).
  • 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) — but a stale public name pointing at nothing is a more visible failure than a stale database, and it is not designed.

References