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

8.2 KiB

status, date, deciders, reconstructed, extends
status date deciders reconstructed extends
accepted 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. ADR 0037 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), 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 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).
  • 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: 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) — but a stale public name pointing at nothing is a more visible failure than a stale database, and it is not designed.

References