fix(mesh): accept node_proxy at the proxy-tunnel WS upgrade (#1050)

The mesh proxy-tunnel WS handler was gated on full-admin api_token scope
only, so node_proxy JWTs (the credential the Add Remote Node dialog tells
operators to generate via Settings -> Nodes -> Generate Token) were
silently rejected with HTTP 403. Operators followed the dialog's
instructions, enrolled the node cleanly, saw reachableMode='proxy' with
no negative badge, opted a stack into mesh, watched the redeploy
complete, and only then discovered no bytes flow.

node_proxy already authorizes every /api/* surface on the remote
(deploy, exec, host console, filesystem), which is a strict superset of
what the mesh proxy-tunnel does. Gating mesh more strictly was theatre,
not security, and it created a UX trap with no in-product signal pointing
at the scope mismatch.

Change the upgrade dispatcher to accept any machine-to-machine
credential: node_proxy JWT or full-admin api_token. Session cookies and
restricted api_token scopes (read-only, deploy-only) remain rejected
through the same paths as before. Adds positive/negative coverage in
upgrade-order.test.ts for all four credential shapes so the gate stays
pinned against future re-tightening. Updates user-facing copy in the
mesh docs and Settings -> API Tokens description so the documented path
matches the actual code.

If we ever introduce a demoted node_proxy variant (audit-only, read-only
fleet member), the right move is differentiated node_proxy scope claims
inside the tunnel JWT (tracker F-A3), not requiring a separate token
type for mesh.
This commit is contained in:
Anso
2026-05-14 23:26:41 -04:00
committed by GitHub
parent 4747b14d2a
commit 946db9df52
6 changed files with 96 additions and 16 deletions
+2 -2
View File
@@ -48,7 +48,7 @@ Four guarantees:
1. **Only opted-in services are reachable.** A stack reaches another stack's services through the mesh only if both stacks have explicitly opted in. The Pilot agent on the target node refuses any request for a non-opted service.
2. **Aliases are not internet-reachable.** Sencho listens on an internal Docker network; nothing about the mesh exposes new ports beyond the host's existing firewall posture.
3. **Traffic is encrypted in transit.** Cross-node bytes ride an authenticated WSS tunnel between Sencho instances. Distributed API nodes additionally require an API token with the **full-admin** scope; tokens with narrower scopes cannot carry mesh traffic.
3. **Traffic is encrypted in transit.** Cross-node bytes ride an authenticated WSS tunnel between Sencho instances. The Node Token generated during fleet enrollment is the credential that authorizes the mesh tunnel; restricted API token scopes (read-only, deploy-only) cannot carry mesh traffic.
4. **Tier-gated and audit-logged.** Only Admiral users can configure the mesh. Enable, disable, opt-in, and opt-out events write durable rows to the audit log with the actor's identity.
What the mesh does **not** do:
@@ -142,7 +142,7 @@ A few things are deliberately out of scope for the first release:
<Accordion title="A Distributed API node shows `unreachable` on the Routing tab">
Central could not open a mesh tunnel to this node. The badge tooltip shows the specific reason. While a node is in this state the **mesh toggle and Add stack to mesh action on the node card are disabled** so a redeploy is not triggered against an unreachable target. Common causes:
- `api token rejected (scope must be full-admin)` — the API token configured for this node is restricted. Open the remote Sencho, generate a token with the **full-admin** scope, and update the node's credentials in **Settings → Nodes**.
- `api token rejected by remote` — the credential central uses to dial this node is not accepted. Open the remote Sencho, generate a fresh Node Token via **Settings → Nodes → Generate Token**, and paste it back into the node's credentials in central's **Settings → Nodes**.
- `remote does not support proxy mesh` — the remote Sencho is on a version that predates proxy-mode mesh. Update the remote and the badge clears on the next refresh.
- `TLS handshake failed` — the remote serves a certificate Node's default trust store does not accept. Use a certificate issued by a trusted authority on the remote.
- `api_url not set` or `api token missing` — the node was added without credentials. Edit the node in **Settings → Nodes** and supply the URL and token.