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
+5 -3
View File
@@ -32,9 +32,10 @@ export function isProxyExemptPath(path: string): boolean {
// nodeId resolves to a remote node so a script/curl call cannot trick the proxy
// into forwarding the request across a node boundary. This matters for the logs
// feed (its admin gate lives in the local route handler, which the proxy would
// skip when forwarding a remote nodeId) and for registries (a proxied registry
// request would otherwise carry a plaintext secret to, or read stored
// credentials from, the remote).
// skip when forwarding a remote nodeId) and for registries and Fleet Secrets (a
// proxied request would otherwise carry a plaintext secret to, or read stored
// secret values from, the remote; the secrets routes' admin gate also lives in
// the local route handler, which the proxy would skip).
//
// Entries are stored with a trailing slash; the matcher accepts the exact
// collection path (without the trailing slash) AND any sub-path under it,
@@ -54,6 +55,7 @@ export const HUB_ONLY_PREFIXES: readonly string[] = [
'/api/logs/global/',
'/api/system/log-stream-metrics/',
'/api/registries/',
'/api/secrets/',
];
/** Returns true when the path is hub-only and must not be proxied to a remote node. */