Issue 116: route-proxy has no authentication or IP-restriction mechanism #109
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user