Files
hq/04-ISSUES/116-route-proxy-has-no-auth-or-ip-restriction/00-report.md
T
jochen a11da86591 ADR 0108: a route carries the policy applied to a request
Issue 116 found the mesh's proxy applies nothing to a request — host lookup, forward. Against
what the replaced ingress actually relies on, four capabilities are missing: authentication
(three dependents, each gating an admin surface with no login of its own), refusal scoped to a
path (one, a live incident mitigation), path-scoped routing with priority, and redirect.

Policy goes on the route rather than beside it. A proxy-side settings layer keyed by route name
would keep the grant literally clean, but then "what protects this route" is answered from two
files nothing keeps in step — and a route's protection is part of what a route is.

The set is closed at those four, so a fifth is an amendment and each addition is earned by a
dependent that exists. An open middleware surface was rejected: it recreates what is being
replaced, and narrowing one later is far harder than widening a closed one.

Where policy needs a credential the declaration names a secret and never carries the value,
which keeps the existing secret machinery the only thing holding credentials. Inlining a hash
was rejected as the first credential in a declaration — a precedent easier to set than withdraw.

This re-keys the routing table by host and path with priority, which follows from the decision
rather than being a separate one: two of the four need one host routed more than one way. Equal
priorities must resolve identically every time or the proxy stops being reproducible.

The record says how it is checked, including the negative case that rots quietly — a
declaration carrying a credential value rather than a reference must be refused, so the
rejected option cannot return by accident.

08-connectivity §3 names the record and gains the subsection; issue 116 gains amended-design.
2026-09-25 13:48:18 +02:00

126 lines
7.1 KiB
Markdown

---
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.