Two things the report got wrong, and one it could not have found the way it looked. Disclosure first: it carried a real hostname and an absolute node path, in a public repository. Both are gone; the ingress, the modules and the routes are named by role, as the rest of 04-ISSUES does. The count was low. Basic authentication has three dependents in the catalogue, not one — the key-value store's browser UI, a database web UI, and the ingress's own dashboard. All three are credential-less admin surfaces whose only gate is a middleware the mesh's proxy lacks. The earlier version read only the node's dynamic configuration directory, which cannot see what modules declare as container labels; counting needs both sources, and the report now says so. Two gaps were missing entirely. Redirect rules: two live routes canonicalise a www name onto its apex, they exist only on the node and not in the catalogue, and they fail silently rather than erroring. And path-scoped routing with priority, which is the one that reorders the issue: the table maps host to exactly one target, so a host cannot be routed two ways, and the refusal rule matches a path on a host already routed elsewhere. Authentication and a source filter would not make it expressible. Path scoping is a prerequisite, not a sibling. Also corrected: the refusal rule was described as an address-scoped deny. It is an allow-list holding a single documentation-range address — deny-everyone — so reading it as address-scoped points at the wrong fix. And its severity was understated: its own header records it as incident response closing an abused write primitive, which is not "a real exposure" but a live mitigation. The open questions now say plainly that they are design questions and the fix should not be written before they are answered, and one is added: whether a declaration may carry a credential at all.
126 lines
7.0 KiB
Markdown
126 lines
7.0 KiB
Markdown
---
|
|
status: located
|
|
opened: 2026-09-25
|
|
located-in: [mesh-controller examples/route-proxy]
|
|
fixed-by:
|
|
amended-design:
|
|
---
|
|
|
|
# 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.
|