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' };