Files
hq/04-ISSUES/140-an-endpoints-reach-is-not-declared/00-report.md
T
jschoubben 14ff89fa40 ADRs 0138 and 0139: an assignment binds an endpoint and says how far it reaches, and a network is forwarded because a module declared it
Both follow from the same rule the mesh is built on — a node's configuration is
composed from the modules assigned to it. Reach was settled separately by the
filter, the proxy's names and the certificate authority, so "this must not be
public" could not be written; it becomes one value on the assignment that all
three read. And the forward chain consulted two constants plus a typed list
although modules already declare their networks; it now forwards what they
declared, with the host rendering the addresses it allocated.
2026-09-28 23:03:32 +02:00

4.8 KiB

status, opened, located-in, fixed-by, amended-design
status opened located-in fixed-by amended-design
located 2026-09-28
mesh-controller internal/catalogue/manifest.go
mesh-controller internal/catalogue/filtering.go
mesh-controller internal/catalogue/declaration.go
mesh-controller examples/route-proxy
mesh-catalog (every routed module manifest)
03-DESIGN/01-to-be/08-connectivity.md

140 — An endpoint's reach is not declared, so three mechanisms each decide it separately

What was observed

Preparing to converge the mesh's control-node — the last machine still running the firewall it had before the mesh — the question came up for one module: the forge serves git over ssh, and that port must stay reachable from outside the private network. Where is that said?

The manifest declares the port with a source of mesh, so the derived filter would close it to everything but the private network. Looking for the place an assignment says otherwise, there are two per-node settings keys: one that gives a module's declared port a machine port, and one that overrides a declared port's source. The second has exactly one caller — the function that builds the node's filter rules. Nothing else in the control plane reads it.

A module's routed endpoint is declared somewhere else entirely: a route contribution naming a label and a port. It says nothing about reach. The proxy composes a public name and an internal name for every route it is given, and obtains a certificate for each from a different authority. Measured on that machine the same day: an identity provider's public name signed by the public authority for 90 days, its internal name signed by the mesh's own intermediate for 24 hours and renewed daily. Both names exist, and both certificates, because the proxy makes every name it can. No assignment asked for either.

So the forge's ssh endpoint has a firewall source and nothing else — no name, no certificate, and no way to say it should be public other than a key the filter alone reads. And the forge's web endpoint has two names and two certificates that nobody requested.

Why it matters beyond this instance

Reach is stated twice, in two vocabularies, in two places that cannot disagree out loud. A port may be exposed to anywhere while the module contributes no public route; a public route may be served for a module whose own listen is private. Nothing reconciles the pair or refuses it. Each mechanism is separately defensible and the combination is unstated.

The vocabulary belongs to the filter, not to reachability. Public, internal, or both cannot be expressed. A source of anywhere is one rule on one chain; it says nothing about which names should exist or which authority should sign them. So "this endpoint must not be public" has no way to be written, and is therefore enforced by nothing — while a public certificate for that very name is obtained automatically.

An endpoint is not a thing in the model. A module has ports, and separately it has routes. Nothing binds a port to a name to a certificate, which is why three mechanisms each decide reach on their own and none of them is wrong. This is ADR 0045's fault one level up: that record closed "a declaration that reads as a restriction and restricts nothing" for the packet filter. Here the declaration is absent altogether and the mechanisms guess.

It blocks the certificate work. The open question recorded for certificates — a name that must not be public needs either DNS-01 or the internal authority only — cannot be answered while no assignment states whether a name should be public. Neither can expiry reporting, revocation, or what happens to a name when a machine leaves: all of them need to know which names were meant.

Open questions

  • Should an assignment name each of a module's endpoints, bind it to a node-level port, and state whether it is reachable publicly, internally or both — with the filter, the proxy's names and the certificate authority all derived from that one statement?
  • What is an endpoint that is neither routed nor certified? Git over ssh is public reach with no name and no certificate; the model has to hold that without inventing one.
  • Are the two existing settings keys the same statement, half-built? If so, is this a new declaration or the completion of theirs?
  • Does an internal-only endpoint get a certificate at all, and from which authority — and does that settle issue 129, where nothing installs the mesh's own root?
  • Does declaring reach per assignment also settle issue 139, where an internal name resolves to the consumer's machine instead of the one serving the endpoint?