diff --git a/agent-cs/AgentWorker.cs b/agent-cs/AgentWorker.cs index 5144806..9a4149f 100644 --- a/agent-cs/AgentWorker.cs +++ b/agent-cs/AgentWorker.cs @@ -6,7 +6,7 @@ namespace ITNexusAgent; public class AgentWorker { - private const string Version = "2.7.0"; + private const string Version = "2.8.0"; private const string DataDir = @"C:\ProgramData\IT Nexus Agent"; private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json"; private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json"; @@ -40,12 +40,24 @@ public class AgentWorker Log($"Agent v{Version} gestartet"); + // Security-Migration: geteilten Bootstrap-Key gegen individuellen Per-Device-Key tauschen. + // Idempotent (Server liefert bestehenden Key erneut) — daher bei jedem Start sicher aufrufbar. + var hostname = SystemInfoService.GetHostname(); + var enrolledKey = await _api.EnrollAsync(hostname); + if (!string.IsNullOrEmpty(enrolledKey) && enrolledKey != _config.AgentKey) + { + _config.AgentKey = enrolledKey; + _config.Save(ConfigPath); + _api.UpdateKey(enrolledKey); + Log("ENROLL: Per-Device-Key erhalten und gespeichert"); + } + // WebSocket Shell-Service im Hintergrund starten - var shellService = new ShellService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname()); + var shellService = new ShellService(_config.ServerUrl, _config.AgentKey, hostname); _ = shellService.RunAsync(_ct); // WebRTC Remote Desktop Service im Hintergrund starten - var rtcService = new RtcService(_config.ServerUrl, _config.AgentKey, SystemInfoService.GetHostname()); + var rtcService = new RtcService(_config.ServerUrl, _config.AgentKey, hostname); _ = rtcService.RunAsync(_ct); while (!_ct.IsCancellationRequested) diff --git a/agent-cs/IT-Nexus-Agent.csproj b/agent-cs/IT-Nexus-Agent.csproj index ccf0953..c19d560 100644 --- a/agent-cs/IT-Nexus-Agent.csproj +++ b/agent-cs/IT-Nexus-Agent.csproj @@ -7,8 +7,8 @@ true IT-Nexus-Agent ITNexusAgent - 2.7.0 - 2.7.0.0 + 2.8.0 + 2.8.0.0 enable enable false diff --git a/agent-cs/Models/Config.cs b/agent-cs/Models/Config.cs index 4e621f7..a148509 100644 --- a/agent-cs/Models/Config.cs +++ b/agent-cs/Models/Config.cs @@ -17,4 +17,9 @@ public class AgentConfig return JsonConvert.DeserializeObject(json) ?? throw new Exception("Ungültige config.json"); } + + public void Save(string path) + { + File.WriteAllText(path, JsonConvert.SerializeObject(this, Formatting.Indented)); + } } diff --git a/agent-cs/Services/ApiService.cs b/agent-cs/Services/ApiService.cs index b43da5d..83c97e0 100644 --- a/agent-cs/Services/ApiService.cs +++ b/agent-cs/Services/ApiService.cs @@ -9,7 +9,25 @@ public class ApiService(string serverUrl, string agentKey) { private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(30) }; private readonly string _baseUrl = serverUrl.TrimEnd('/'); - private readonly string _agentKey = agentKey; + private string _agentKey = agentKey; + + // Nach erfolgreichem Enrollment wird der geteilte Bootstrap-Key durch den individuellen + // Per-Device-Key ersetzt — alle nachfolgenden Requests dieser Instanz nutzen ab dann den neuen Key. + public void UpdateKey(string newKey) => _agentKey = newKey; + + public async Task EnrollAsync(string hostname) + { + try + { + var req = BuildRequest(HttpMethod.Post, "/api/monitoring/enroll", new { hostname }); + var resp = await _http.SendAsync(req); + if (!resp.IsSuccessStatusCode) return null; + var body = await resp.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeAnonymousType(body, new { status = "", data = new { agent_key = "" } }); + return result?.data?.agent_key; + } + catch { return null; } + } private HttpRequestMessage BuildRequest(HttpMethod method, string path, object? body = null) { diff --git a/agent-cs/setup.iss b/agent-cs/setup.iss index 1ca0615..ff2fbe0 100644 --- a/agent-cs/setup.iss +++ b/agent-cs/setup.iss @@ -1,5 +1,5 @@ #define MyAppName "IT Nexus Agent" -#define MyAppVersion "2.7.0" +#define MyAppVersion "2.8.0" #define MyAppPublisher "Cereda Systems GmbH" #define MyAppURL "https://it-nexus.cereda-systems.de" #define MyAppExeName "IT-Nexus-Agent.exe" diff --git a/backend/src/controllers/monitoringAgent.controller.js b/backend/src/controllers/monitoringAgent.controller.js index 5a4b43d..b558874 100644 --- a/backend/src/controllers/monitoringAgent.controller.js +++ b/backend/src/controllers/monitoringAgent.controller.js @@ -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 }; diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index 991dd56..557fe71 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -449,6 +449,8 @@ async function initializeDatabase() { `ALTER TABLE monitoring_agents ADD COLUMN defender_enabled INTEGER DEFAULT 0`, `ALTER TABLE monitoring_agents ADD COLUMN defender_signatures_age INTEGER DEFAULT -1`, `ALTER TABLE monitoring_agents ADD COLUMN hardware_serial TEXT`, + // Pro-Geräte Agent-Key (Security-Migration weg vom geteilten AGENT_API_KEY) + `ALTER TABLE monitoring_agents ADD COLUMN agent_key TEXT`, // Sichere Links (Shares) `CREATE TABLE IF NOT EXISTS shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/backend/src/routes/monitoringAgent.routes.js b/backend/src/routes/monitoringAgent.routes.js index 5f161f2..9aec459 100644 --- a/backend/src/routes/monitoringAgent.routes.js +++ b/backend/src/routes/monitoringAgent.routes.js @@ -7,6 +7,9 @@ const { requireAdmin } = require('../middleware/roleCheck'); // Agent checkin — no JWT, uses X-Agent-Key header router.post('/checkin', ctrl.checkin); +// Agent tauscht Bootstrap-Key gegen individuellen Per-Device-Key (Security-Migration) +router.post('/enroll', ctrl.enroll); + // Schneller Announcement-Poll (leichtgewichtig, alle 15 Sek) router.post('/announcements-poll', ctrl.announcementsPoll); diff --git a/backend/src/utils/agentAuth.js b/backend/src/utils/agentAuth.js new file mode 100644 index 0000000..e1036e2 --- /dev/null +++ b/backend/src/utils/agentAuth.js @@ -0,0 +1,18 @@ +const { getDatabase } = require('../config/database'); + +// Übergangslogik pro-Geräte-Keys (DSGVO/Security-Migration): +// - Alte Agents (vor v2.8.0) schicken noch den einen geteilten AGENT_API_KEY → wird vorübergehend +// weiter akzeptiert, damit der Rollout nicht die ganze Fleet auf einmal bricht. +// - Migrierte Agents schicken ihren individuellen, beim Enrollment ausgestellten Key — dieser wird +// NUR für den exakt zugehörigen Hostname akzeptiert, ein gestohlener Key kann also nicht mehr +// benutzt werden, um sich als ein anderes Gerät auszugeben. +function validateAgentKey(presentedKey, hostname) { + if (!presentedKey) return false; + if (presentedKey === process.env.AGENT_API_KEY) return true; + if (!hostname) return false; + const db = getDatabase(); + const row = db.prepare('SELECT agent_key FROM monitoring_agents WHERE hostname = ?').get(hostname); + return !!row?.agent_key && row.agent_key === presentedKey; +} + +module.exports = { validateAgentKey }; diff --git a/backend/src/ws/shellServer.js b/backend/src/ws/shellServer.js index cafefd1..ef4bef8 100644 --- a/backend/src/ws/shellServer.js +++ b/backend/src/ws/shellServer.js @@ -1,6 +1,7 @@ const WebSocket = require('ws'); const jwt = require('jsonwebtoken'); const { getDatabase } = require('../config/database'); +const { validateAgentKey } = require('../utils/agentAuth'); // agentId -> WebSocket const agentSockets = new Map(); @@ -43,12 +44,12 @@ function setupWebSocketServer(httpServer) { function handleAgent(ws, url) { const key = url.searchParams.get('key'); - if (key !== process.env.AGENT_API_KEY) { + const hostname = url.searchParams.get('hostname'); + if (!hostname) { ws.close(1008, 'hostname required'); return; } + if (!validateAgentKey(key, hostname)) { ws.close(1008, 'unauthorized'); return; } - const hostname = url.searchParams.get('hostname'); - if (!hostname) { ws.close(1008, 'hostname required'); return; } const db = getDatabase(); const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname); @@ -143,9 +144,9 @@ function handleBrowser(ws, url) { function handleRdpAgent(ws, url) { const key = url.searchParams.get('key'); - if (key !== process.env.AGENT_API_KEY) { ws.close(1008, 'unauthorized'); return; } const hostname = url.searchParams.get('hostname'); if (!hostname) { ws.close(1008, 'hostname required'); return; } + if (!validateAgentKey(key, hostname)) { ws.close(1008, 'unauthorized'); return; } const db = getDatabase(); const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname); diff --git a/frontend/src/pages/PatchManagementPage.jsx b/frontend/src/pages/PatchManagementPage.jsx index 0532e34..6627acf 100644 --- a/frontend/src/pages/PatchManagementPage.jsx +++ b/frontend/src/pages/PatchManagementPage.jsx @@ -5,7 +5,7 @@ import { useAuth } from '../context/AuthContext'; const ANN_TYPES = { maintenance: { icon: '🔧', label: 'Wartung', color: '#f59e0b' }, warning: { icon: '⚠️', label: 'Warnung', color: '#ef4444' }, info: { icon: 'ℹ️', label: 'Info', color: '#6366f1' } }; -const LATEST_AGENT_VERSION = '2.7.0'; +const LATEST_AGENT_VERSION = '2.8.0'; const SEVERITY_LABELS = { critical: 'Kritisch', important: 'Wichtig', moderate: 'Moderat', low: 'Niedrig', all: 'Alle' }; const SEVERITY_COLORS = { critical: '#EF4444', important: '#F59E0B', moderate: '#3B82F6', low: '#6B7280', all: '#0D9488' }; const COMMAND_LABELS = { check_updates: '🔍 Update-Scan', install_updates: '⬇️ Installation', reboot: '🔄 Neustart', upgrade_win11: '🪟 Win 11 Upgrade', update_agent: '⬆️ Agent-Update' };