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

Merged
jschoubben merged 4 commits from issue/116-route-proxy-has-no-auth-or-ip-restriction into main 2026-09-25 12:28:58 +00:00
4 changed files with 139 additions and 2 deletions
Showing only changes of commit a11da86591 - Show all commits
@@ -0,0 +1,102 @@
---
topic: the tiers
status: accepted
date: 2026-09-25
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0007-connectivity.md
---
# 108. A route carries the policy applied to a request, and names a secret rather than holding one
## Context
[ADR 0007](0007-connectivity.md) settled that **a route is a grant**: a module contributes the name
it wants and the port it listens on, and the proxy hands back the public name. That governs the
**grant**. It says nothing about what a request arriving at the name is permitted to do, and the
mesh's proxy currently permits everything: its request path is a host lookup and a forward, with no
authentication, no source check, no redirect handling and no middleware anywhere in it.
The standing requirement is that the mesh does **at minimum** what the system it replaces already
does. Measured against the predecessor's live configuration and its module catalogue
([issue 116](../04-ISSUES/116-route-proxy-has-no-auth-or-ip-restriction/00-report.md)), four
capabilities are relied on and absent:
| Capability | Dependents, counted |
|---|---|
| Authentication | **three** modules, each gating a credential-less admin surface — a key-value browser UI, a **database web UI**, and the replaced ingress's own dashboard |
| Refusal scoped to a path | **one**, and it is a live **incident mitigation** closing a write primitive that was abused |
| Path-scoped routing with priority | **two** — the refusal above, and a certificate-challenge path on a host that otherwise routes elsewhere |
| Redirect | **two** live routes canonicalising a `www` name onto its apex |
Counting needed two sources and neither alone is complete: the catalogue cannot see what was
hand-written on a node, and a node's configuration directory cannot see what modules declare as
container labels. An earlier count read one source and undercounted authentication by two.
**The third row is a prerequisite, not a sibling.** The proxy's table maps a host to exactly one
target, so a host cannot be routed two ways. Adding authentication and a source filter would not
make the refusal rule expressible.
## Considered Options
1. **Leave policy out of the mesh; keep the affected routes on the adopted ingress.** *Rejected.*
The mesh would run two reverse proxies indefinitely with no principled division between them, and
one of the routes held back is a live mitigation — leaving it on a component being decommissioned
means its removal date is whenever somebody forgets.
2. **A separate, proxy-side settings layer keyed by route name.** *Rejected.* It keeps the grant
literally clean, but answering *"what protects this route"* then requires reading two files that
nothing keeps in step. A route's protection is part of what a route is.
3. **The grant carries a reference; the detail lives in a second layer.** *Rejected.* Both costs of
option 2, plus a naming indirection to maintain.
4. **Carry the password hash in the declaration.** *Rejected.* It would be the first credential
value in a declaration, and a precedent is easier to set than to withdraw. A hash is not a
plaintext password, but it is sufficient to pass the gate it protects.
5. **An open middleware surface the proxy applies.** *Rejected.* It recreates the thing being
replaced, makes the proxy's behaviour unbounded, and an open surface is far harder to narrow later
than a closed one is to widen.
## Decision
**A route contribution carries the policy applied to requests arriving at its name**, alongside the
name, the port and the location it already carries.
**The set is closed, and it is these four:** authentication; refusal scoped to a path; path-scoped
routing with priority; redirect. A fifth is an amendment to this record, deliberately — each
addition should be earned by a dependent that exists.
**Where policy needs a credential, the declaration names a secret the mesh mints and holds. It never
carries the value.** This keeps the existing secret machinery as the only thing that holds
credentials, and keeps hashes out of anything regenerated, synced or committed.
**Consequently the routing table is keyed by host and path, with priority** — not by host alone.
This follows from the decision rather than being a separate one: two of the four capabilities need a
single host routed more than one way.
## Consequences
**What this makes possible.** The affected routes can leave the adopted ingress, and "at minimum"
becomes a satisfiable claim rather than a standing exception. The incident mitigation gets a durable
home in the mesh, which its own note already asked for.
**What got harder.** The contribution shape grows, and every provider of `route` must understand
more of it. The table is no longer a flat map, and priority introduces ordering that has to be
deterministic rather than incidental — equal priorities must resolve the same way every time or the
proxy becomes non-reproducible. A closed set means a new need is a decision, not a patch.
**What does not change.** The proxy remains a reference implementation: the contract is the file the
mesh writes, not the program that reads it. Another proxy may implement the same file.
**How this is checked.** A rule states how it is verified, so this one does. A lab bed must show,
against a mesh that declared them: a route with authentication refusing an unauthenticated request
and admitting an authenticated one; a path-scoped refusal shadowing an ordinary route on the same
host while that ordinary route still serves every other path; a redirect answering with the
redirect; and — the negative case, which is the one that rots quietly — **a declaration carrying a
credential value rather than a reference being refused**, so option 4 cannot return by accident.
## References
- [Issue 116](../04-ISSUES/116-route-proxy-has-no-auth-or-ip-restriction/00-report.md) — the count,
the evidence, and the structural finding about the table.
- [ADR 0007](0007-connectivity.md) — a route is a grant. This record extends it to the request.
- [ADR 0009](0009-modules-and-the-graph.md) — the provision vocabulary a contribution belongs to.
- [To-be 08 §3](../03-DESIGN/01-to-be/08-connectivity.md) — where exposure is specified.
+1
View File
@@ -123,6 +123,7 @@ python3 00-META/checks/index.py fail if stale
- **0094** — [A module may hold several secrets from one provider, each a pair of its own](0094-a-module-may-hold-several-secrets-from-one-provider.md)
- **0095** — [The control plane is the way to ask a module](0095-the-control-plane-is-the-way-to-ask-a-module.md)
- **0098** — [A fact a provider makes at first start is fetched from it, not carried in its manifest](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)
- **0108** — [A route carries the policy applied to a request, and names a secret rather than holding one](0108-a-route-carries-the-policy-applied-to-a-request.md)
### What runs on them, and how it gets there
+35 -1
View File
@@ -7,11 +7,12 @@ code:
- mesh-controller internal/identity/authority.go
- mesh-host internal/identity/serving.go
- mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-09-23
updated: 2026-09-25
decisions:
- 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md
- 02-DECISIONS/0108-a-route-carries-the-policy-applied-to-a-request.md
- 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
- 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md
@@ -442,6 +443,39 @@ exactly the per-module cost it is meant to remove. The design is the composition
stopgap until the manifest layer can carry a label and a domain separately
([ADR 0066](../../02-DECISIONS/0066-public-routing-is-name-agnostic.md)).
### A route also carries what a request arriving at it may do
*2026-09-25, from comparing the mesh's proxy against the ingress it would replace
([issue 116](../../04-ISSUES/116-route-proxy-has-no-auth-or-ip-restriction/00-report.md)),
decided in [ADR 0108](../../02-DECISIONS/0108-a-route-carries-the-policy-applied-to-a-request.md).*
A grant hands back a name. It did not say what the name admits, and the proxy admitted everything —
its request path was a host lookup and a forward. Measured against what the replaced ingress
actually relies on, four things were missing: **authentication**, **refusal scoped to a path**,
**path-scoped routing with priority**, and **redirect**. Three modules depend on the first, each to
gate an admin surface that has no login of its own; one dependent of the second is a live incident
mitigation.
**Policy belongs to the route, not beside it.** A contribution carries it along with the name, the
port and the location. The alternative — a proxy-side settings layer keyed by route name — keeps the
grant literally clean but makes *"what protects this route"* a question answered from two files that
nothing keeps in step. A route's protection is part of what a route is.
**The set is closed at those four.** A fifth is an amendment, so each addition is earned by a
dependent that exists rather than added because a middleware surface was open. An open surface would
recreate the thing being replaced, and is far harder to narrow later than a closed one is to widen.
**Where policy needs a credential, the declaration names a secret; it never carries one.** The mesh
already mints and holds credentials, and that machinery stays the only thing that does — so a hash
never reaches anything regenerated, synced or committed.
**This re-keys the table.** Two of the four need one host routed more than one way, so the proxy
matches on host **and path**, with priority, rather than mapping a host to a single target. Equal
priorities must resolve identically every time, or the proxy stops being reproducible.
The proxy remains a reference implementation: the contract is the file the mesh writes, not the
program that reads it, and another proxy may implement the same file.
## 4 — Filtering
**Derived from what is assigned here, and from the overlay's shape** — a node's open ports are a
@@ -3,7 +3,7 @@ status: located
opened: 2026-09-25
located-in: [mesh-controller examples/route-proxy]
fixed-by:
amended-design:
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