--- status: located opened: 2026-09-25 located-in: [mesh-controller examples/route-proxy] fixed-by: amended-design: 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](../../03-DESIGN/01-to-be/08-connectivity.md)) 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](../004-certificate-issuance-targets-production/00-report.md)) — stricter than the hand-maintained configuration it would replace. - **Several public names for one module.** Already solved by the `contributes` many-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 These are design questions, not implementation details, and the fix should not be written before they are answered. - **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.