From b1e0bd9548305c79e599b1474dd9bf1357998765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Gr=C3=BCssing?= Date: Fri, 26 Jun 2026 14:27:56 +0200 Subject: [PATCH] =?UTF-8?q?Security:=20Agent-Key=20in=20config.json=20per?= =?UTF-8?q?=20Windows=20DPAPI=20verschl=C3=BCsselt=20(v2.9.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent_key liegt jetzt nicht mehr im Klartext auf der Platte, sondern via ProtectedData.Protect (DataProtectionScope.LocalMachine) verschlüsselt — nur das SYSTEM-Konto auf genau diesem einen Rechner kann den Wert wieder entschlüsseln. Reines Auslesen von config.json bringt einem lokalen Angreifer/Malware also nichts mehr. Migration automatisch beim ersten Start von v2.9.0: erkennt das alte Klartext-Format, verschlüsselt beim nächsten Save() automatisch — kein manueller Eingriff nötig, läuft über den bestehenden Staged-Rollout. Co-Authored-By: Claude Sonnet 4.6 --- agent-cs/AgentWorker.cs | 7 +-- agent-cs/IT-Nexus-Agent.csproj | 5 ++- agent-cs/Models/Config.cs | 52 ++++++++++++++++++++-- agent-cs/setup.iss | 2 +- frontend/src/pages/PatchManagementPage.jsx | 2 +- 5 files changed, 57 insertions(+), 11 deletions(-) diff --git a/agent-cs/AgentWorker.cs b/agent-cs/AgentWorker.cs index 9a4149f..80bc9ab 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.8.0"; + private const string Version = "2.9.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"; @@ -44,10 +44,11 @@ public class AgentWorker // 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) + var configWasUnencrypted = !File.ReadAllText(ConfigPath).Contains("dpapi:"); + if (!string.IsNullOrEmpty(enrolledKey) && (enrolledKey != _config.AgentKey || configWasUnencrypted)) { _config.AgentKey = enrolledKey; - _config.Save(ConfigPath); + _config.Save(ConfigPath); // schreibt agent_key jetzt DPAPI-verschlüsselt statt im Klartext _api.UpdateKey(enrolledKey); Log("ENROLL: Per-Device-Key erhalten und gespeichert"); } diff --git a/agent-cs/IT-Nexus-Agent.csproj b/agent-cs/IT-Nexus-Agent.csproj index c19d560..53e3981 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.8.0 - 2.8.0.0 + 2.9.0 + 2.9.0.0 enable enable false @@ -31,6 +31,7 @@ + diff --git a/agent-cs/Models/Config.cs b/agent-cs/Models/Config.cs index a148509..a13282c 100644 --- a/agent-cs/Models/Config.cs +++ b/agent-cs/Models/Config.cs @@ -1,25 +1,69 @@ using Newtonsoft.Json; using System.IO; +using System.Security.Cryptography; +using System.Text; namespace ITNexusAgent.Models; public class AgentConfig { - [JsonProperty("server_url")] + private const string ProtectedPrefix = "dpapi:"; + + [JsonIgnore] public string ServerUrl { get; set; } = ""; - [JsonProperty("agent_key")] + // Im Speicher immer Klartext — nur auf der Platte (config.json) liegt der verschlüsselte Wert. + [JsonIgnore] public string AgentKey { get; set; } = ""; + private class RawConfig + { + [JsonProperty("server_url")] public string ServerUrl { get; set; } = ""; + [JsonProperty("agent_key")] public string AgentKey { get; set; } = ""; + } + public static AgentConfig Load(string path) { var json = File.ReadAllText(path); - return JsonConvert.DeserializeObject(json) + var raw = JsonConvert.DeserializeObject(json) ?? throw new Exception("Ungültige config.json"); + + return new AgentConfig + { + ServerUrl = raw.ServerUrl, + AgentKey = Unprotect(raw.AgentKey), + }; } + // Verschlüsselt den Key per Windows DPAPI (LocalMachine-Scope) bevor er auf die Platte geschrieben + // wird — ein Klartext-Auslesen von config.json bringt einem Angreifer dann nichts mehr, da der Wert + // nur vom SYSTEM-Konto auf genau diesem Rechner wieder entschlüsselt werden kann. public void Save(string path) { - File.WriteAllText(path, JsonConvert.SerializeObject(this, Formatting.Indented)); + var raw = new RawConfig { ServerUrl = ServerUrl, AgentKey = Protect(AgentKey) }; + File.WriteAllText(path, JsonConvert.SerializeObject(raw, Formatting.Indented)); + } + + private static string Protect(string plaintext) + { + if (string.IsNullOrEmpty(plaintext)) return plaintext; + var bytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(plaintext), null, DataProtectionScope.LocalMachine); + return ProtectedPrefix + Convert.ToBase64String(bytes); + } + + // Erkennt das alte Klartext-Format (z.B. frisch aus dem Installer-Template) und lässt es unverändert + // durch — wird beim nächsten Save() automatisch verschlüsselt persistiert. + private static string Unprotect(string stored) + { + if (string.IsNullOrEmpty(stored) || !stored.StartsWith(ProtectedPrefix)) return stored; + try + { + var bytes = ProtectedData.Unprotect(Convert.FromBase64String(stored[ProtectedPrefix.Length..]), null, DataProtectionScope.LocalMachine); + return Encoding.UTF8.GetString(bytes); + } + catch + { + return stored; // Korrupt/falsche Maschine → unverändert zurückgeben statt Crash + } } } diff --git a/agent-cs/setup.iss b/agent-cs/setup.iss index ff2fbe0..db406c7 100644 --- a/agent-cs/setup.iss +++ b/agent-cs/setup.iss @@ -1,5 +1,5 @@ #define MyAppName "IT Nexus Agent" -#define MyAppVersion "2.8.0" +#define MyAppVersion "2.9.0" #define MyAppPublisher "Cereda Systems GmbH" #define MyAppURL "https://it-nexus.cereda-systems.de" #define MyAppExeName "IT-Nexus-Agent.exe" diff --git a/frontend/src/pages/PatchManagementPage.jsx b/frontend/src/pages/PatchManagementPage.jsx index 6627acf..87bb778 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.8.0'; +const LATEST_AGENT_VERSION = '2.9.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' };