Security: Pro-Geräte Agent-Keys statt geteiltem AGENT_API_KEY (v2.8.0)
Agent v2.8.0 tauscht beim Start automatisch den geteilten Bootstrap-Key gegen einen individuellen Per-Device-Key (POST /api/monitoring/enroll, idempotent). Checkin/Announcements-Poll/Setup-Download/WS-Agent-Verbindungen validieren den Key jetzt gegen den jeweiligen Hostname — ein gestohlener Key kann sich nicht mehr als anderer Agent ausgeben (manuell verifiziert). Alte Agents mit dem geteilten Key funktionieren während der Übergangsphase weiter (validateAgentKey() akzeptiert beides), damit der Rollout die Fleet nicht abrupt bricht — Migration läuft über den bestehenden Staged-Rollout (Test → Pilot → Produktion). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,37 @@ const { asyncHandler } = require('../middleware/errorHandler');
|
||||
const { getPendingCommands } = require('./patch.controller');
|
||||
const { getDatabase } = require('../config/database');
|
||||
const { getForAgent } = require('./announcement.controller');
|
||||
const { validateAgentKey } = require('../utils/agentAuth');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// POST /api/monitoring/enroll — Agent tauscht geteilten Bootstrap-Key gegen individuellen Per-Device-Key.
|
||||
// Idempotent: ein bereits enrollter Agent bekommt seinen bestehenden Key einfach erneut zurück.
|
||||
const enroll = asyncHandler(async (req, res) => {
|
||||
const agentKey = req.headers['x-agent-key'];
|
||||
const { hostname } = req.body;
|
||||
if (!hostname) return res.status(400).json({ status: 'error', message: 'hostname required' });
|
||||
|
||||
const db = getDatabase();
|
||||
const existing = db.prepare('SELECT id, agent_key FROM monitoring_agents WHERE hostname = ?').get(hostname);
|
||||
|
||||
const validBootstrap = agentKey === process.env.AGENT_API_KEY;
|
||||
const validExisting = existing?.agent_key && agentKey === existing.agent_key;
|
||||
if (!validBootstrap && !validExisting) {
|
||||
return res.status(401).json({ status: 'error', message: 'Invalid agent key' });
|
||||
}
|
||||
|
||||
let key = existing?.agent_key;
|
||||
if (!key) {
|
||||
key = 'itx-' + crypto.randomBytes(24).toString('hex');
|
||||
if (existing) {
|
||||
db.prepare('UPDATE monitoring_agents SET agent_key = ? WHERE id = ?').run(key, existing.id);
|
||||
} else {
|
||||
db.prepare('INSERT INTO monitoring_agents (hostname, agent_key) VALUES (?, ?)').run(hostname, key);
|
||||
}
|
||||
console.log(`[Enroll] Neuer Per-Device-Key ausgestellt für ${hostname}`);
|
||||
}
|
||||
res.json({ status: 'success', data: { agent_key: key } });
|
||||
});
|
||||
|
||||
function extractManufacturer(modelStr) {
|
||||
if (!modelStr) return null;
|
||||
@@ -75,14 +106,13 @@ function syncAgentToAsset(data) {
|
||||
// POST /api/monitoring/checkin — called by agent (no JWT, uses API key)
|
||||
const checkin = asyncHandler(async (req, res) => {
|
||||
const agentKey = req.headers['x-agent-key'];
|
||||
if (!agentKey || agentKey !== process.env.AGENT_API_KEY) {
|
||||
return res.status(401).json({ status: 'error', message: 'Invalid agent key' });
|
||||
}
|
||||
|
||||
const { hostname } = req.body;
|
||||
if (!hostname) {
|
||||
return res.status(400).json({ status: 'error', message: 'hostname required' });
|
||||
}
|
||||
if (!validateAgentKey(agentKey, hostname)) {
|
||||
return res.status(401).json({ status: 'error', message: 'Invalid agent key' });
|
||||
}
|
||||
|
||||
const agent = MonitoringAgent.upsert(req.body);
|
||||
|
||||
@@ -133,11 +163,11 @@ const getAll = asyncHandler(async (req, res) => {
|
||||
// POST /api/monitoring/announcements-poll — leichter Poll nur für Ankündigungen
|
||||
const announcementsPoll = asyncHandler(async (req, res) => {
|
||||
const agentKey = req.headers['x-agent-key'];
|
||||
if (!agentKey || agentKey !== process.env.AGENT_API_KEY) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const { hostname } = req.body;
|
||||
if (!hostname) return res.status(400).json({ error: 'hostname required' });
|
||||
if (!validateAgentKey(agentKey, hostname)) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
const agent = MonitoringAgent.getByHostname(hostname);
|
||||
if (!agent) return res.json({ announcements: [] });
|
||||
const announcements = getForAgent(agent.id);
|
||||
@@ -204,7 +234,7 @@ const downloadAnnWatcher = asyncHandler(async (req, res) => {
|
||||
const downloadSetup = asyncHandler(async (req, res) => {
|
||||
// Agent-Key Auth für auto-update vom Agent selbst
|
||||
const agentKey = req.headers['x-agent-key'];
|
||||
const isAgent = agentKey && agentKey === process.env.AGENT_API_KEY;
|
||||
const isAgent = validateAgentKey(agentKey, req.query.hostname);
|
||||
const isAdmin = req.user?.role_name === 'admin' || req.user?.role_name === 'super_admin';
|
||||
if (!isAgent && !isAdmin) {
|
||||
return res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||
@@ -242,4 +272,4 @@ const downloadSetup = asyncHandler(async (req, res) => {
|
||||
res.sendFile(setupPath);
|
||||
});
|
||||
|
||||
module.exports = { checkin, announcementsPoll, getAll, getStatistics, getById, deleteAgent, downloadScript, downloadAnnWatcher, downloadSetup };
|
||||
module.exports = { checkin, announcementsPoll, getAll, getStatistics, getById, deleteAgent, downloadScript, downloadAnnWatcher, downloadSetup, enroll };
|
||||
|
||||
Reference in New Issue
Block a user