diff --git a/04-ISSUES/116-route-proxy-has-no-auth-or-ip-restriction/00-report.md b/04-ISSUES/116-route-proxy-has-no-auth-or-ip-restriction/00-report.md new file mode 100644 index 0000000..a814001 --- /dev/null +++ b/04-ISSUES/116-route-proxy-has-no-auth-or-ip-restriction/00-report.md @@ -0,0 +1,74 @@ +--- +status: located +opened: 2026-09-25 +located-in: [mesh-controller examples/route-proxy] +fixed-by: +amended-design: +--- + +# 116 — `route-proxy` has no authentication or IP-restriction mechanism, and two live HAL routes depend on one + +## What was observed + +On the control-node, 2026-09-25, deciding whether `route-proxy` (novox/hq 08-connectivity §3, +`mesh-controller/examples/route-proxy/main.go`) can replace HAL's adopted Traefik instance as the +permanent public ingress. The standing requirement is that the nox mesh does, at minimum, what the +HAL mesh it is replacing already does — so the comparison is against Traefik's *current, real* +configuration on this node, not Traefik's general feature set. + +Reading `/services/traefik/dynamic/` on the control-node directly, two of its routes carry +middleware `route-proxy` has nothing to match: + +``` +redisinsight.novox.be → traefik.http.middlewares.redisinsight-auth.basicauth.users: + (RedisInsight has no login of its own; this is the only gate in front of it) +gitea-internal → zz-security-gitea-internal-deny.yml — an IP-scoped deny rule +``` + +`route-proxy`'s entire request path is `main.go`'s `handler()`: one lookup into the routing table +built from `$ROUTES`, then `proxy.ServeHTTP(w, r)` — no middleware chain, no auth check, no source +filtering, anywhere in the file. Confirmed by reading the whole of `main.go` (261 lines): the only +things it does per-request are route lookup and proxy. It was designed this way deliberately — "a +route hands back a name, not a credential" (the module's own README) — but that design covers the +*route grant*, not what a request arriving at the name is allowed to do, which is what these two +HAL routes need. + +Everything else compared cleanly or better, and is **not** part of this issue: + +- Streaming/large request bodies (why Traefik needed `minio-api-body-size: maxRequestBodyBytes: + 0` for the object store's routes): `net/http/httputil.ReverseProxy` streams with no default cap, + so this need requires nothing extra here. +- WebSocket/connection upgrades (used by at least one console route): native in + `httputil.ReverseProxy` since Go 1.12; not yet verified live against this proxy, but not absent + by design the way auth is. +- TLS/ACME: `route-proxy`'s own `HostPolicy` (`onlyWhatTheMeshSaid`) refuses to certify anything + the mesh did not actually route, and defaults to Let's Encrypt staging until a node opts in to + production (novox/hq 04-ISSUES/004) — stricter than the hand-maintained Traefik config it would + replace. +- Multiple public names for one module (minio's `files-api` / `files`): already solved by the + `contributes` many-shape (mesh-controller PR #55), needs nothing further from the proxy. + +## Why it matters + +Two routes cannot move off Traefik until this is closed, and — under the standing "at minimum" +rule — nothing wholesale can be called a Traefik replacement while it is. Left unfixed, either: + +- those two routes stay on `route-adapter`/Traefik indefinitely, which means the mesh is running + two reverse proxies side by side for no principled reason, or +- somebody migrates them anyway and RedisInsight (no auth of its own) or the gitea-internal path + (currently IP-restricted) becomes reachable to anyone who can resolve the name — a real exposure, + not a cosmetic gap. + +## Open questions + +- Where does the auth/restriction config come from? The contract today is "one contribution, a + name and a port" (`main.go`'s `contribution.Values`) — extending it to optionally carry an + htpasswd hash or an allowed-CIDR list keeps the mesh (not the proxy) as the source of truth, + consistent with everything else `route` already does. An alternative — a second, proxy-specific + settings layer, keyed by route name — decouples it from the route grant itself but adds a second + place to look. Worth deciding deliberately rather than defaulting to whichever is less code. +- Basic auth and an IP allow/deny list cover what HAL's config uses today. Whether that is the + right *general* shape for route-level middleware, or just the two cases observed so far, is + itself worth a decision before writing it — the module's own README already states "the contract + is the file, not this program," so nothing here requires all of it to live in this one reference + implementation.