fix(cluster): shared CLUSTER_READ_TOKEN so mam-api sees containers on ALL nodes

/cluster/containers only returned the primary's containers: mam-api fanned out
to each node-agent with a single NODE_AGENT_TOKEN, but each node-agent only
accepted its own bound NODE_TOKEN, so remote nodes returned 401 and were
silently dropped (UI showed 'only zampp1').

node-agent now ALSO accepts a shared CLUSTER_READ_TOKEN (= mam-api's
NODE_AGENT_TOKEN) for the read-only container/log endpoints, so the aggregate
container view + per-container logs work across the whole cluster.
This commit is contained in:
Zac Gaetano 2026-06-04 05:14:44 +00:00
parent d6b0b3a9a6
commit 70c873ae95
2 changed files with 20 additions and 8 deletions

View file

@ -47,6 +47,10 @@ services:
environment: environment:
MAM_API_URL: ${MAM_API_URL} MAM_API_URL: ${MAM_API_URL}
NODE_TOKEN: ${NODE_TOKEN:-} NODE_TOKEN: ${NODE_TOKEN:-}
# Shared cluster-read token: lets the primary mam-api fan-out read-only
# container/log queries to every node with one token (= mam-api's
# NODE_AGENT_TOKEN). Set identically across the cluster.
CLUSTER_READ_TOKEN: ${CLUSTER_READ_TOKEN:-}
NODE_ROLE: ${NODE_ROLE:-worker} NODE_ROLE: ${NODE_ROLE:-worker}
# NODE_NAME pins the cluster identity (heartbeat key). Set it per-node so # NODE_NAME pins the cluster identity (heartbeat key). Set it per-node so
# cloned VMs that share /etc/hostname don't collide on the same # cloned VMs that share /etc/hostname don't collide on the same

View file

@ -941,19 +941,27 @@ async function handleSidecarStatus(containerId, res) {
// When NODE_TOKEN is configured, privileged control endpoints (driver install) // When NODE_TOKEN is configured, privileged control endpoints (driver install)
// require a matching `Authorization: Bearer <NODE_TOKEN>`. mam-api forwards the // require a matching `Authorization: Bearer <NODE_TOKEN>`. mam-api forwards the
// node's stored token. If NODE_TOKEN is empty (dev), auth is not enforced. // node's stored token. If NODE_TOKEN is empty (dev), auth is not enforced.
//
// A SHARED cluster-read token (CLUSTER_READ_TOKEN) is ALSO accepted so the
// primary mam-api can fan-out read-only cluster queries (container list, logs)
// to every node with ONE token, rather than tracking each node's bound token.
// It only grants the same endpoints NODE_TOKEN does; set it identically on
// mam-api (NODE_AGENT_TOKEN) and every node-agent.
const CLUSTER_READ_TOKEN = process.env.CLUSTER_READ_TOKEN || '';
function _bearerEq(token, secret) {
if (!secret || token.length !== secret.length) return false;
try { return crypto.timingSafeEqual(Buffer.from(token), Buffer.from(secret)); }
catch (_) { return false; }
}
function checkAgentAuth(req) { function checkAgentAuth(req) {
if (!NODE_TOKEN) return true; if (!NODE_TOKEN && !CLUSTER_READ_TOKEN) return true;
const hdr = req.headers['authorization'] || ''; const hdr = req.headers['authorization'] || '';
const m = /^Bearer\s+(.+)$/i.exec(hdr); const m = /^Bearer\s+(.+)$/i.exec(hdr);
if (!m) return false; if (!m) return false;
const token = m[1]; const token = m[1];
if (token.length !== NODE_TOKEN.length) return false; return _bearerEq(token, NODE_TOKEN) || _bearerEq(token, CLUSTER_READ_TOKEN);
try {
return crypto.timingSafeEqual(Buffer.from(token), Buffer.from(NODE_TOKEN));
} catch (_) {
return false;
}
} }
// ── Driver/SDK install ──────────────────────────────────────────────────── // ── Driver/SDK install ────────────────────────────────────────────────────