Skip to content

Commit fb159a3

Browse files
Skiipy11claude
andcommitted
security: per-agent API keys with identity enforcement
Agent registry loaded from env vars (AGENT_KEY_<name>=<key>): - AGENT_KEY_claude_code=xxx → authenticates as "claude-code" - AGENT_KEY_n8n=yyy → authenticates as "n8n" - AGENT_KEY_morpheus=zzz → authenticates as "morpheus" When an agent key is used, source_agent in POST /memory must match the authenticated identity — prevents agent impersonation. Admin key (BRAIN_API_KEY) still works as fallback with no identity binding, preserving backward compatibility. Closes the Sybil attack vector on observed_by: an agent can no longer corroborate its own memories by claiming different identities. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fa78ec7 commit fb159a3

2 files changed

Lines changed: 50 additions & 5 deletions

File tree

api/src/middleware/auth.js

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import crypto from 'crypto';
22

3-
const API_KEY = process.env.BRAIN_API_KEY;
3+
const ADMIN_KEY = process.env.BRAIN_API_KEY;
4+
5+
// Build agent registry from env vars: AGENT_KEY_<name>=<key>
6+
// e.g. AGENT_KEY_claude_code=abc123 → { key: 'abc123', agent: 'claude-code' }
7+
const agentRegistry = new Map(); // key → agent name
8+
9+
function loadAgentKeys() {
10+
for (const [envKey, envVal] of Object.entries(process.env)) {
11+
if (envKey.startsWith('AGENT_KEY_') && envVal) {
12+
// AGENT_KEY_claude_code → claude-code
13+
const agentName = envKey.slice('AGENT_KEY_'.length).replace(/_/g, '-').toLowerCase();
14+
agentRegistry.set(envVal, agentName);
15+
}
16+
}
17+
if (agentRegistry.size > 0) {
18+
console.log(`[auth] Loaded ${agentRegistry.size} agent key(s): ${[...agentRegistry.values()].join(', ')}`);
19+
}
20+
}
21+
22+
loadAgentKeys();
423

524
// Rate limiting: track failed auth attempts per IP
625
const failedAttempts = new Map();
@@ -28,6 +47,11 @@ function recordFailure(ip) {
2847
}
2948
}
3049

50+
function safeEqual(a, b) {
51+
if (!a || !b || a.length !== b.length) return false;
52+
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
53+
}
54+
3155
export function authMiddleware(req, res, next) {
3256
const ip = req.ip || req.socket.remoteAddress;
3357

@@ -36,10 +60,24 @@ export function authMiddleware(req, res, next) {
3660
}
3761

3862
const key = req.headers['x-api-key'];
39-
if (!key || !API_KEY || key.length !== API_KEY.length ||
40-
!crypto.timingSafeEqual(Buffer.from(key), Buffer.from(API_KEY))) {
63+
if (!key) {
4164
recordFailure(ip);
42-
return res.status(401).json({ error: 'Invalid or missing API key' });
65+
return res.status(401).json({ error: 'Missing API key' });
66+
}
67+
68+
// Check agent-specific keys first (binds identity)
69+
const agentName = agentRegistry.get(key);
70+
if (agentName) {
71+
req.authenticatedAgent = agentName;
72+
return next();
4373
}
44-
next();
74+
75+
// Fall back to admin key (no agent binding — full access)
76+
if (safeEqual(key, ADMIN_KEY)) {
77+
req.authenticatedAgent = null; // admin — no agent identity enforced
78+
return next();
79+
}
80+
81+
recordFailure(ip);
82+
return res.status(401).json({ error: 'Invalid API key' });
4583
}

api/src/routes/memory.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ memoryRouter.post('/', async (req, res) => {
2626
return res.status(400).json({ error: validationError });
2727
}
2828

29+
// Enforce agent identity: if authenticated with an agent key, source_agent must match
30+
if (req.authenticatedAgent && source_agent !== req.authenticatedAgent) {
31+
return res.status(403).json({
32+
error: `Agent identity mismatch: authenticated as "${req.authenticatedAgent}" but source_agent is "${source_agent}"`,
33+
});
34+
}
35+
2936
// Scrub credentials
3037
const cleanContent = scrubCredentials(content);
3138

0 commit comments

Comments
 (0)