fix(fleet-secrets): restrict bundle management to admin hub sessions (#1274)

* fix(fleet-secrets): restrict bundle management to admin hub sessions

Fleet Secrets exposes decrypted environment-variable values and writes
credentials across the fleet, so every route now requires an admin role,
runs only on the instance you are signed into, and rejects long-lived API
tokens:

- Add an admin-role check to all secrets routes; the frontend Secrets tab
  renders only for admin users so the affordance matches the backend gate.
- Add /api/secrets/ to the hub-only path list so a request carrying a
  remote node id cannot be proxied to read another node's decrypted values.
- Reject API tokens on every secrets route (browser admin sessions only),
  matching how registry credentials are handled.

Also adds lifecycle and developer-mode diagnostic logging (never the secret
values) and tests covering the admin boundary on every endpoint, API-token
rejection, hub-only enforcement, and diagnostic gating.

* fix(fleet-secrets): require a signed-in user session for all secrets routes

The earlier API-token rejection only blocked opaque API tokens. node_proxy
and pilot_tunnel JWTs are mapped to an admin role by the auth middleware
without an API-token scope, so they still passed the admin gate and could
read decrypted bundles via GET /api/secrets/:id.

Replace the API-token check with requireUserSession, which rejects API tokens
and node_proxy / pilot_tunnel machine credentials (userId 0) on every secrets
route, returning SESSION_REQUIRED. The admin role is still enforced after.

Tests now assert SESSION_REQUIRED for a full-admin API token across all nine
routes and for node_proxy and pilot_tunnel JWTs.

* test(fleet-secrets): mint the rejection-test token via the real endpoint

The machine-credential test reconstructed an API token by sha256-hashing a
raw key inline. That duplicated a hashing sink that CodeQL's
js/insufficient-password-hash query flags (a false positive for a 256-bit
random token, but a new occurrence in the diff). Create the token through
POST /api/api-tokens instead, so the hashing stays in the production path
and the test carries none of its own. Behavior and coverage are unchanged.
This commit is contained in:
Anso
2026-06-01 19:47:06 -04:00
committed by GitHub
parent 53be6a258e
commit 9fb4ccccff
7 changed files with 227 additions and 9 deletions
+14
View File
@@ -50,6 +50,20 @@ export const requireAdmin = (req: Request, res: Response): boolean => {
return true;
};
/**
* Require a genuine signed-in user session. Rejects opaque API tokens
* (`req.apiTokenScope`) and node_proxy / pilot_tunnel machine credentials,
* which authMiddleware maps to role 'admin' with userId 0. Endpoints that
* expose decrypted secrets use this so a long-lived machine credential cannot
* reach them; the admin role itself is still enforced separately by requireAdmin.
*/
export const requireUserSession = (req: Request, res: Response): boolean => {
if (req.apiTokenScope || !req.user || req.user.userId === 0) {
return deny(res, 'SESSION_REQUIRED', 'This action requires a signed-in user session.');
}
return true;
};
/**
* Accept only calls from a sibling Sencho using its node_proxy Bearer token.
* Browser sessions, API tokens, and console tokens are all rejected.