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
+1 -1
View File
@@ -87,7 +87,7 @@ export const SETTINGS_ITEMS: readonly SettingsItemMeta[] = [
id: 'api-tokens',
group: 'identity',
label: 'API Tokens',
description: 'Long-lived bearer tokens for CI, scripts, and remote nodes.',
description: 'Long-lived bearer tokens for CI and scripts.',
keywords: ['bearer', 'automation', 'ci', 'scripts', 'scopes'],
tier: 'admiral',
scope: 'global',