Issue 116: route-proxy has no authentication or IP-restriction mechanism

Comparing route-proxy against what HAL's actual Traefik config does today,
not Traefik's general feature set, per the standing rule that the nox mesh
must do at minimum what the HAL mesh it replaces already does. Everything
else checked out even or better; these two are real, confirmed gaps —
RedisInsight has no login of its own and depends entirely on Traefik's
basicauth middleware, and the gitea-internal route depends on an IP-scoped
deny rule. Neither has any equivalent in route-proxy's single-lookup
request path.
This commit is contained in:
2026-09-25 11:51:56 +02:00
parent cdcd4da27e
commit 226d556743
@@ -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: <htpasswd hash>
(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.