The gap is closed in the proxy: policy applies, the four capabilities exist, the table is keyed by host and path with a total ordering, and the two failure modes that rot quietly are held by tests — a declaration carrying a credential refused rather than served, an unreadable secret failing closed. Resolved rather than left open because the issue reports a gap in the proxy and that gap is gone. But the record says plainly what it does not yet allow: an operator still cannot move the affected routes, because that needs the mesh side — a manifest able to declare these values and the controller minting the secret auth names. Until both exist the capability is reachable only by writing the routes file by hand. That is the ordinary build-out of a contract this issue's decision created, and it belongs to to-be 08 rather than here. The open questions are marked answered and kept rather than deleted, pointing at ADR 0108 — what was rejected and why is the half worth having, and a section still saying "the fix should not be written before these are answered" after the fix was written reads as though nobody looked. One finding kept in the record: priority was read with the reader for ports, which caps at 65535, and the one real rule this reproduces is declared at 100000. It parsed to zero, so refusal and path scoping would have shipped looking complete and doing nothing on the only case that motivated them. A validator borrowed from a neighbouring field is a silent default.
9.2 KiB
status, opened, located-in, fixed-by, amended-design
| status | opened | located-in | fixed-by | amended-design | |
|---|---|---|---|---|---|
| resolved | 2026-09-25 |
|
mesh-controller PR | 03-DESIGN/01-to-be/08-connectivity.md |
116 — The mesh's proxy applies no policy to a request: no authentication, no source restriction, no path scoping, no redirects
What was observed
On 2026-09-25, deciding whether the mesh's own reverse proxy (to-be 08) can replace the ingress the mesh adopted from the predecessor, as the permanent public entry point. The standing requirement is that the mesh does at minimum what the system it replaces already does, so the comparison is against the predecessor's real, live configuration and its module catalogue — not against a feature list.
The proxy's entire request path is a host lookup and a forward. Its handler takes the request's host, finds a target in a table, and either answers a named 404 or hands the request to the reverse proxy. There is no authentication check, no source-address check, no redirect handling and no middleware chain anywhere in the program.
The table is the reason this is structural rather than a missing feature: it maps host → one target, and the lookup strips the port and lowercases the host. A host cannot be routed two ways.
The design is deliberate as far as it goes — "a route hands back a name, not a credential" — but that governs the route grant. It says nothing about what a request arriving at that name is allowed to do, which is what the live configuration relies on.
What the predecessor actually relies on, counted
Two sources, because neither alone is complete: the module catalogue, and the ingress's own dynamic configuration on the node. The catalogue misses what was hand-written on the node; the node's directory misses what modules declare as container labels. An earlier version of this report read only the latter and undercounted as a result.
1. Basic authentication — three dependents in the catalogue
| Module | What it is the only gate on |
|---|---|
| the key-value store | its browser UI, which has no login of its own |
| the relational store | a database web UI |
| the ingress itself | its own dashboard — twice, counting a desktop flavour |
Every one is a credential-less admin surface whose sole protection is a middleware the mesh's proxy does not have. The database web UI is the worst of the three, and the ingress dashboard means the ingress is currently protecting itself with a mechanism its replacement lacks.
2. Outright refusal on a path — an incident mitigation
One hand-written rule on the node blocks external access to an internal API path on the forge. Its own header records it as incident response to a compromise, closing the write primitive that was abused. It is expressed as an allow-list containing a single documentation-range address — that is, a deny-everyone — and the middleware is named accordingly.
It is not an address-scoped allow-list in any useful sense, and reading it as one points at the wrong fix. What it needs is the ability to refuse a request outright, scoped to a path.
The same header already records where this belongs: "not mesh-managed. Durable home is the route-proxy module; re-home when convenient."
3. Path-scoped routing with priority — and this one gates the others
That rule matches a path prefix on a host that is already routed elsewhere, and carries an explicit high priority so it shadows the ordinary route. The mail module needs the same shape for a different reason: it routes a certificate-challenge path on a host that otherwise goes to the mail front end.
Because the table maps a host to exactly one target, neither is expressible today, and adding authentication and a source filter would not make them so. Path scoping with priority is a prerequisite for the refusal rule, not a feature beside it.
4. Redirect rules — live, and they fail quietly
Two routes canonicalise a www name onto its apex with a rewriting redirect. They appear in the
node's configuration and not in the catalogue, so a catalogue-only survey misses them. They are
the easiest of the four to lose, because losing them produces no error — just two public names that
quietly stop redirecting.
What compared cleanly, and is not part of this issue
- Large and streaming request bodies. Four modules raise or remove the body cap — the object store, the file-sync application, the image registry. The standard library's reverse proxy streams with no default cap, so this needs nothing added.
- Connection upgrades, used by at least one console route: native to the standard library's reverse proxy. Not yet verified live against this proxy, but not absent by design the way the four gaps above are.
- Certificate issuance. The proxy refuses to certify any name the mesh did not route, and defaults to a staging issuer until a node opts in to production (issue 004) — stricter than the hand-maintained configuration it would replace.
- Several public names for one module. Already solved by the
contributesmany-shape; needs nothing from the proxy.
Why it matters
Under the "at minimum" rule, nothing can be called a replacement for the predecessor's ingress while any of the four is missing — and one of them is a live mitigation for an exploited vulnerability. The two outcomes if it is left unfixed are both bad:
- the affected routes stay on the adopted ingress indefinitely, leaving the mesh running two reverse proxies side by side with no principled division between them; or
- they are migrated anyway, and three credential-less admin surfaces become reachable by anyone who can resolve a name, while a known-exploited path loses the block that was put in front of it during an incident.
Open questions — answered
These were design questions, not implementation details, and the fix was not written before they were answered. All three were settled in ADR 0108: policy goes on the route, the set is closed at the four, and a declaration names a secret and never carries one. Kept as asked, because what was rejected and why is the half worth having.
- Where does request-level policy come from? Today a contribution carries a name and a port. Extending it to carry policy keeps the mesh as the source of truth, consistent with everything else a route already does. A separate proxy-side settings layer keyed by route name decouples policy from the grant but adds a second place to look. The module's own README notes "the contract is the file, not this program", so this is a contract decision and not a property of one reference implementation.
- May a declaration carry a credential? A password hash in a contribution puts a secret in a declaration. That cuts across how the mesh mints and holds secrets, and it should be settled deliberately rather than as a side effect of whichever option is less code.
- Is the right general shape "these four", or something narrower? Authentication, refusal, path scoping and redirects are what the predecessor uses today. Whether route-level policy should be an open middleware surface, or exactly these four and no more, is worth deciding before any of it is written — an open surface is far harder to withdraw than to add.
What the fix covers, and what it does not yet allow
2026-09-25, on resolution. The gap this issue reports is closed: the proxy applies policy, the four capabilities exist, the table is keyed by host and path with a total ordering, and the two failure modes that would rot quietly are held by tests — a declaration carrying a credential is refused rather than served, and an unreadable secret makes the route refuse rather than open.
It does not yet let an operator move the affected routes. That needs the mesh side: a manifest
able to declare these values, and the controller minting the secret that auth names. Until both
exist the capability is reachable only by writing the routes file by hand, so the routes held back
on the adopted ingress stay there.
Resolved rather than left open because the issue reports a gap in the proxy, and that gap is gone. The remaining work is not this fault persisting; it is the ordinary build-out of a contract this record's decision created, and it belongs to to-be 08 rather than here.
One thing found while fixing it, worth keeping. Priority was first read with the reader for ports, which caps at 65535 — and the one real rule this has to reproduce is declared at 100000. It parsed to zero, so refusal and path scoping would both have shipped looking complete, passing their own tests, and doing nothing on the only case that motivated them. A validator borrowed from a neighbouring field is a silent default, and the test that now guards it goes through the proxy, because at the parser the value looked fine.