diff --git a/Fuchs/Docs/keyvault-dpapi-ocore-implementation.md b/Fuchs/Docs/keyvault-dpapi-ocore-implementation.md deleted file mode 100644 index b087e9a..0000000 --- a/Fuchs/Docs/keyvault-dpapi-ocore-implementation.md +++ /dev/null @@ -1,933 +0,0 @@ -# Agent Instructions: Key Vault + DPAPI — Multi-App Architektur (15 Projekte, 1 Server) - -## Ausgangslage - -- 15 Anwendungen (Web Apps, Console Apps, Windows Services, Worker Services) -- Alle laufen auf demselben Windows-Server -- Ein gemeinsamer Azure Key Vault -- Secrets sind aktuell in `appsettings.json`, `web.config` oder Umgebungsvariablen -- Zwei bestehende Shared Projects: **OCORE** und **OCORE_web** - ---- - -## Aufteilung zwischen OCORE und OCORE_web - -``` -OCORE OCORE_web -────────────────────────────── ────────────────────────────── -Alles was NICHT Web-spezifisch Nur was WebApplicationBuilder -ist. Funktioniert für: benötigt. Funktioniert für: - - - Console Apps - ASP.NET Core Web Apps - - Windows Services - ASP.NET Core Web APIs - - Worker Services - Razor Pages / MVC - - WinForms / WPF - - Web Apps (via OCORE_web) - -Enthält: Enthält: - AppSecretsOptions SecretManagementWebExtensions - DpapiSecretsCache (eine einzige Datei) - DpapiCacheConfigurationSource - KeyVaultSyncService - PrefixKeyVaultSecretManager - SecretManagementExtensions - (IHostBuilder) -``` - -> OCORE_web referenziert OCORE — d.h. Web Apps brauchen nur OCORE_web -> zu referenzieren, OCORE kommt transitiv mit. - ---- - -## Architektur-Übersicht - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Azure Key Vault │ -│ │ -│ crm--ConnectionStrings--Database → CRM Web App │ -│ crm--ExternalApi--ApiKey → CRM Web App │ -│ erp--ConnectionStrings--Database → ERP Web App │ -│ erp--Smtp--Password → ERP Web App │ -│ importer--ConnectionStrings--Db → Importer Console App │ -│ scheduler--ConnectionStrings--Db → Scheduler Windows Svc │ -│ ... │ -└──────────────────────────┬──────────────────────────────────────┘ - │ Managed Identity (Server-Level) -┌──────────────────────────▼──────────────────────────────────────┐ -│ Windows Server │ -│ │ -│ C:\inetpub\crm\ secrets.cache (DPAPI, crm-Entropy) │ -│ C:\inetpub\erp\ secrets.cache (DPAPI, erp-Entropy) │ -│ C:\services\importer\ secrets.cache (DPAPI, importer-Entropy) │ -│ C:\services\scheduler\secrets.cache (DPAPI, scheduler-Entropy)│ -│ ... │ -│ │ -│ OCORE.dll ← alle 15 Apps referenzieren dies │ -│ OCORE_web.dll ← nur Web Apps referenzieren dies zusätzlich │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Wichtige Designentscheidungen - -| Thema | Entscheidung | Begründung | -|---|---|---| -| Key Vault | Ein gemeinsamer | Einfacher zu verwalten, RBAC auf Secret-Ebene möglich | -| Namespacing | `{appname}--{key}` Präfix | Verhindert Kollisionen zwischen Apps | -| DPAPI Scope | `LocalMachine` + App-spezifische Entropy | Isolation: App A kann Cache von App B nicht lesen | -| Basiscode | OCORE | Kein Web-Framework nötig, funktioniert überall | -| Web-Extension | OCORE_web | Nur eine Datei, dünne Schicht über OCORE | -| App-Identity | Eine Server-Managed-Identity | Alle Apps teilen dieselbe Identity, RBAC per Secret | - ---- - -## Teil 1: OCORE — NuGet-Pakete hinzufügen - -```bash -cd OCORE -dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets -dotnet add package Azure.Identity -dotnet add package Azure.Security.KeyVault.Secrets -dotnet add package Microsoft.Extensions.Hosting.Abstractions -dotnet add package Microsoft.Extensions.Configuration.Abstractions -dotnet add package Microsoft.Extensions.Logging.Abstractions -``` - ---- - -## Teil 2: OCORE — Dateien anlegen - -Neue Ordnerstruktur **innerhalb von OCORE** (bestehende Struktur bleibt unverändert): - -``` -OCORE/ - ... (bestehende Dateien) - Secrets/ - Models/ - AppSecretsOptions.cs - Cache/ - DpapiSecretsCache.cs - DpapiCacheConfigurationSource.cs - Sync/ - KeyVaultSyncService.cs - Extensions/ - SecretManagementExtensions.cs ← IHostBuilder Extension + gemeinsame Logik - PrefixKeyVaultSecretManager.cs -``` - ---- - -### 2.1 OCORE/Secrets/Models/AppSecretsOptions.cs - -```csharp -namespace OCORE.Secrets.Models; - -public class AppSecretsOptions -{ - public const string SectionName = "SecretManagement"; - - /// - /// Vollständige URI des Azure Key Vault. - /// Beispiel: https://company-vault.vault.azure.net/ - /// - public string VaultUri { get; set; } = string.Empty; - - /// - /// App-Name als Präfix für alle Secrets in Key Vault. - /// Beispiel: "crm" → Secret "crm--ConnectionStrings--Database" - /// Nur Kleinbuchstaben und Bindestriche (Key Vault Konvention). - /// - public string AppName { get; set; } = string.Empty; - - /// - /// Pfad zur DPAPI-Cache-Datei (relativ zum App-Verzeichnis). - /// - public string CacheFilePath { get; set; } = "secrets.cache"; - - /// - /// Sync-Intervall in Stunden. - /// - public int SyncIntervalHours { get; set; } = 6; - - /// - /// Secret-Namen ohne App-Präfix. - /// Beispiel: ["ConnectionStrings--Database", "ExternalApi--ApiKey"] - /// - public List ManagedSecretKeys { get; set; } = []; -} -``` - ---- - -### 2.2 OCORE/Secrets/Cache/DpapiSecretsCache.cs - -```csharp -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using Microsoft.Extensions.Logging; - -namespace OCORE.Secrets.Cache; - -public interface IDpapiSecretsCache -{ - Task WriteAsync(Dictionary secrets); - Task> ReadAsync(); - bool Exists(); -} - -public class DpapiSecretsCache : IDpapiSecretsCache -{ - private readonly string _filePath; - private readonly byte[] _entropy; - private readonly ILogger _logger; - - /// - /// Entropy wird aus AppName abgeleitet — verhindert dass eine andere App - /// auf demselben Server den Cache dieser App lesen kann. - /// - public DpapiSecretsCache(string filePath, string appName, ILogger logger) - { - _filePath = filePath; - _entropy = Encoding.UTF8.GetBytes($"dpapi-entropy-{appName}"); - _logger = logger; - } - - public async Task WriteAsync(Dictionary secrets) - { - try - { - var json = JsonSerializer.SerializeToUtf8Bytes(secrets); - var encrypted = ProtectedData.Protect(json, _entropy, DataProtectionScope.LocalMachine); - await File.WriteAllBytesAsync(_filePath, encrypted); - _logger.LogInformation("[OCORE.Secrets] DPAPI Cache geschrieben: {Count} Secrets → {Path}", - secrets.Count, _filePath); - } - catch (Exception ex) - { - _logger.LogError(ex, "[OCORE.Secrets] DPAPI Cache konnte nicht geschrieben werden: {Path}", _filePath); - } - } - - public async Task> ReadAsync() - { - if (!Exists()) return []; - - try - { - var encrypted = await File.ReadAllBytesAsync(_filePath); - var decrypted = ProtectedData.Unprotect(encrypted, _entropy, DataProtectionScope.LocalMachine); - return JsonSerializer.Deserialize>(decrypted) ?? []; - } - catch (CryptographicException ex) - { - _logger.LogError(ex, - "[OCORE.Secrets] DPAPI Entschlüsselung fehlgeschlagen — AppName korrekt? {Path}", _filePath); - return []; - } - catch (Exception ex) - { - _logger.LogError(ex, "[OCORE.Secrets] DPAPI Cache Lesefehler: {Path}", _filePath); - return []; - } - } - - public bool Exists() => File.Exists(_filePath); -} -``` - ---- - -### 2.3 OCORE/Secrets/Cache/DpapiCacheConfigurationSource.cs - -```csharp -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace OCORE.Secrets.Cache; - -public class DpapiCacheConfigurationSource : IConfigurationSource -{ - private readonly string _filePath; - private readonly string _appName; - - public DpapiCacheConfigurationSource(string filePath, string appName) - { - _filePath = filePath; - _appName = appName; - } - - public IConfigurationProvider Build(IConfigurationBuilder builder) - => new DpapiCacheConfigurationProvider(_filePath, _appName); -} - -public class DpapiCacheConfigurationProvider : ConfigurationProvider -{ - private readonly string _filePath; - private readonly string _appName; - - public DpapiCacheConfigurationProvider(string filePath, string appName) - { - _filePath = filePath; - _appName = appName; - } - - public override void Load() - { - if (!File.Exists(_filePath)) return; - - try - { - using var loggerFactory = LoggerFactory.Create(b => b.AddConsole()); - var logger = loggerFactory.CreateLogger(); - var cache = new DpapiSecretsCache(_filePath, _appName, logger); - var secrets = cache.ReadAsync().GetAwaiter().GetResult(); - - // "crm--ConnectionStrings--Database" → "ConnectionStrings:Database" - var prefix = $"{_appName}--"; - Data = secrets - .Where(kvp => kvp.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - .ToDictionary( - kvp => kvp.Key[prefix.Length..].Replace("--", ":"), - kvp => (string?)kvp.Value); - } - catch (Exception ex) - { - Console.WriteLine($"[OCORE.Secrets] DPAPI Cache Ladefehler: {ex.Message}"); - Data = []; - } - } -} -``` - ---- - -### 2.4 OCORE/Secrets/Sync/KeyVaultSyncService.cs - -```csharp -using Azure.Security.KeyVault.Secrets; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using OCORE.Secrets.Cache; -using OCORE.Secrets.Models; - -namespace OCORE.Secrets.Sync; - -/// -/// Läuft als BackgroundService in jedem Host-Typ: -/// Web Apps, Console Apps, Windows Services, Worker Services. -/// Synct Secrets aus Key Vault → DPAPI-Cache, periodisch. -/// -public class KeyVaultSyncService : BackgroundService -{ - private readonly SecretClient _secretClient; - private readonly IDpapiSecretsCache _cache; - private readonly IConfigurationRoot _configRoot; - private readonly AppSecretsOptions _options; - private readonly ILogger _logger; - - public KeyVaultSyncService( - SecretClient secretClient, - IDpapiSecretsCache cache, - IConfiguration configuration, - IOptions options, - ILogger logger) - { - _secretClient = secretClient; - _cache = cache; - _configRoot = (IConfigurationRoot)configuration; - _options = options.Value; - _logger = logger; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await SyncAsync(stoppingToken); - - using var timer = new PeriodicTimer(TimeSpan.FromHours(_options.SyncIntervalHours)); - while (await timer.WaitForNextTickAsync(stoppingToken)) - { - await SyncAsync(stoppingToken); - } - } - - private async Task SyncAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("[OCORE.Secrets:{App}] Key Vault Sync gestartet", _options.AppName); - var synced = new Dictionary(); - - foreach (var key in _options.ManagedSecretKeys) - { - var fullName = $"{_options.AppName}--{key}"; - try - { - var secret = await _secretClient.GetSecretAsync(fullName, version: null, cancellationToken); - synced[fullName] = secret.Value.Value; - } - catch (Exception ex) - { - _logger.LogWarning("[OCORE.Secrets:{App}] Secret '{Name}' nicht geladen: {Error}", - _options.AppName, fullName, ex.Message); - } - } - - if (synced.Count > 0) - { - await _cache.WriteAsync(synced); - _configRoot.Reload(); - _logger.LogInformation("[OCORE.Secrets:{App}] Sync OK: {Count}/{Total}", - _options.AppName, synced.Count, _options.ManagedSecretKeys.Count); - } - else - { - _logger.LogWarning("[OCORE.Secrets:{App}] Sync: Keine Secrets geladen — Cache unverändert", - _options.AppName); - } - } -} -``` - ---- - -### 2.5 OCORE/Secrets/Extensions/PrefixKeyVaultSecretManager.cs - -```csharp -using Azure.Extensions.AspNetCore.Configuration.Secrets; -using Azure.Security.KeyVault.Secrets; - -namespace OCORE.Secrets.Extensions; - -/// -/// Filtert beim Key Vault Laden nur Secrets mit dem App-Präfix -/// und konvertiert den Namen in einen IConfiguration-Key. -/// "crm--ConnectionStrings--Database" → "ConnectionStrings:Database" -/// -public class PrefixKeyVaultSecretManager : KeyVaultSecretManager -{ - private readonly string _prefix; - - public PrefixKeyVaultSecretManager(string appName) - => _prefix = $"{appName}--"; - - public override bool Load(SecretProperties secret) - => secret.Name.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase); - - public override string GetKey(KeyVaultSecret secret) - => secret.Name[_prefix.Length..].Replace("--", ":"); -} -``` - ---- - -### 2.6 OCORE/Secrets/Extensions/SecretManagementExtensions.cs - -```csharp -using Azure.Identity; -using Azure.Security.KeyVault.Secrets; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using OCORE.Secrets.Cache; -using OCORE.Secrets.Models; -using OCORE.Secrets.Sync; - -namespace OCORE.Secrets.Extensions; - -public static class SecretManagementExtensions -{ - /// - /// Für Console Apps, Worker Services, Windows Services, WinForms/WPF. - /// Verwendung: Host.CreateDefaultBuilder(args).AddSecretManagement() - /// - public static IHostBuilder AddSecretManagement(this IHostBuilder hostBuilder) - { - hostBuilder.ConfigureAppConfiguration((context, config) => - { - var built = config.Build(); - var options = built.GetSection(AppSecretsOptions.SectionName).Get() - ?? new AppSecretsOptions(); - - if (!IsConfigured(options)) return; - - var cacheFilePath = ResolveCachePath( - context.HostingEnvironment.ContentRootPath, options.CacheFilePath); - - // 1. DPAPI-Cache (Fallback) - config.Add(new DpapiCacheConfigurationSource(cacheFilePath, options.AppName)); - - // 2. Key Vault (primär — überschreibt Cache wenn erreichbar) - TryAddKeyVault(config, options); - }); - - hostBuilder.ConfigureServices((context, services) => - { - RegisterCoreServices(services, context.Configuration); - }); - - return hostBuilder; - } - - /// - /// Gemeinsame Service-Registrierung — wird von OCORE und OCORE_web verwendet. - /// - public static void RegisterCoreServices(IServiceCollection services, IConfiguration configuration) - { - var options = configuration - .GetSection(AppSecretsOptions.SectionName) - .Get() ?? new AppSecretsOptions(); - - if (!IsConfigured(options)) return; - - services.Configure( - configuration.GetSection(AppSecretsOptions.SectionName)); - - services.AddSingleton(new SecretClient( - new Uri(options.VaultUri), - new DefaultAzureCredential())); - - services.AddSingleton(sp => - new DpapiSecretsCache( - ResolveCachePath(AppContext.BaseDirectory, options.CacheFilePath), - options.AppName, - sp.GetRequiredService>())); - - services.AddHostedService(); - } - - public static void TryAddKeyVault(IConfigurationBuilder config, AppSecretsOptions options) - { - try - { - config.AddAzureKeyVault( - new Uri(options.VaultUri), - new DefaultAzureCredential(), - new PrefixKeyVaultSecretManager(options.AppName)); - } - catch (Exception ex) - { - Console.WriteLine($"[OCORE.Secrets] Key Vault nicht erreichbar: {ex.Message}"); - Console.WriteLine("[OCORE.Secrets] Starte mit DPAPI-Cache"); - } - } - - public static bool IsConfigured(AppSecretsOptions options) - => !string.IsNullOrEmpty(options.VaultUri) && !string.IsNullOrEmpty(options.AppName); - - public static string ResolveCachePath(string basePath, string cacheFilePath) - => Path.IsPathRooted(cacheFilePath) - ? cacheFilePath - : Path.Combine(basePath, cacheFilePath); -} -``` - ---- - -## Teil 3: OCORE_web — Einzige neue Datei - -OCORE_web referenziert OCORE bereits. Nur eine neue Datei hinzufügen: - -``` -OCORE_web/ - ... (bestehende Dateien) - Secrets/ - SecretManagementWebExtensions.cs ← einzige neue Datei in OCORE_web -``` - -### 3.1 OCORE_web/Secrets/SecretManagementWebExtensions.cs - -```csharp -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using OCORE.Secrets.Extensions; -using OCORE.Secrets.Models; - -namespace OCORE_web.Secrets; - -public static class SecretManagementWebExtensions -{ - /// - /// Für ASP.NET Core Web Apps und Web APIs. - /// Verwendung: builder.AddSecretManagement() - /// - public static WebApplicationBuilder AddSecretManagement( - this WebApplicationBuilder builder) - { - var options = builder.Configuration - .GetSection(AppSecretsOptions.SectionName) - .Get() ?? new AppSecretsOptions(); - - if (!SecretManagementExtensions.IsConfigured(options)) return builder; - - var cacheFilePath = SecretManagementExtensions.ResolveCachePath( - builder.Environment.ContentRootPath, options.CacheFilePath); - - // 1. DPAPI-Cache (Fallback) - builder.Configuration.Add( - new OCORE.Secrets.Cache.DpapiCacheConfigurationSource(cacheFilePath, options.AppName)); - - // 2. Key Vault (primär) - SecretManagementExtensions.TryAddKeyVault(builder.Configuration, options); - - // 3. Services — aus OCORE (kein duplizierter Code) - SecretManagementExtensions.RegisterCoreServices(builder.Services, builder.Configuration); - - return builder; - } -} -``` - ---- - -## Teil 4: Verwendung je App-Typ - -### Web App (referenziert OCORE_web) - -```csharp -// Program.cs -using OCORE_web.Secrets; - -var builder = WebApplication.CreateBuilder(args); -builder.AddSecretManagement(); // ← aus OCORE_web - -var app = builder.Build(); -app.Run(); -``` - -### Console App / Worker Service / Windows Service (referenziert OCORE) - -```csharp -// Program.cs -using OCORE.Secrets.Extensions; - -// Console / Worker -var host = Host.CreateDefaultBuilder(args) - .AddSecretManagement() // ← aus OCORE - .ConfigureServices(services => - { - services.AddHostedService(); - }) - .Build(); - -await host.RunAsync(); -``` - -```csharp -// Windows Service — identisch, nur UseWindowsService() zusätzlich -var host = Host.CreateDefaultBuilder(args) - .UseWindowsService() - .AddSecretManagement() // ← aus OCORE - .ConfigureServices(services => - { - services.AddHostedService(); - }) - .Build(); - -await host.RunAsync(); -``` - -### WinForms / WPF (referenziert OCORE) - -```csharp -// Program.cs — Generic Host nachrüsten -using OCORE.Secrets.Extensions; - -var host = Host.CreateDefaultBuilder() - .AddSecretManagement() // ← aus OCORE - .ConfigureServices(services => - { - services.AddSingleton(); - }) - .Build(); - -Application.Run(host.Services.GetRequiredService()); -``` - ---- - -## Teil 5: appsettings.json (identisch für alle App-Typen) - -```json -{ - "SecretManagement": { - "VaultUri": "https://company-vault.vault.azure.net/", - "AppName": "crm", - "CacheFilePath": "secrets.cache", - "SyncIntervalHours": 6, - "ManagedSecretKeys": [ - "ConnectionStrings--Database", - "ExternalApi--ApiKey" - ] - }, - "ConnectionStrings": { - "Database": "MANAGED_BY_KEYVAULT" - } -} -``` - -> `AppName` ist der einzige Wert der sich zwischen den 15 Projekten unterscheidet. - ---- - -## Teil 6: Key Vault Naming-Konvention - -``` -Format: {appname}--{ConfigSection}--{ConfigKey} - -Web Apps: - crm--ConnectionStrings--Database - crm--ExternalApi--ApiKey - erp--ConnectionStrings--Database - erp--Smtp--Password - portal--Auth--JwtSecret - -Non-Web Apps: - importer--ConnectionStrings--Database - scheduler--ConnectionStrings--Database - scheduler--Smtp--Password -``` - -### Naming-Regeln - -- Nur alphanumerische Zeichen und einfache Bindestriche `-` -- Hierarchie-Trenner: `--` (doppelter Bindestrich) -- Kein Unterstrich, kein Punkt, kein Leerzeichen -- Maximal 127 Zeichen - ---- - -## Teil 7: Einmalige Migration — Alle Apps - -### 7.1 Inventur-Script - -```powershell -# inventarisierung.ps1 -param([string]$SolutionRoot = "C:\Projects\MySolution") - -$patterns = @( - 'password\s*=\s*["\'][^"\']+["\']', - 'connectionstring\s*=\s*["\'][^"\']+["\']', - 'apikey\s*=\s*["\'][^"\']+["\']', - 'secret\s*=\s*["\'][^"\']+["\']', - 'pwd\s*=\s*[^;]+' -) - -Get-ChildItem -Path $SolutionRoot -Recurse -Include "*.json","*.config","*.cs" | - Where-Object { $_.FullName -notmatch '\\(bin|obj|\.git)\\' } | - ForEach-Object { - $file = $_ - $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue - foreach ($pattern in $patterns) { - if ($content -match $pattern) { - Write-Host "⚠️ $($file.FullName)" -ForegroundColor Yellow - break - } - } - } -``` - -### 7.2 Migration Script - -```powershell -# migrate-all-to-keyvault.ps1 -param([Parameter(Mandatory)][string]$VaultName) - -Connect-AzAccount - -$allSecrets = @{ - # Web Apps - "crm--ConnectionStrings--Database" = "Server=PROD-SQL;Database=CrmDb;..." - "crm--ExternalApi--ApiKey" = "crm-api-key" - "erp--ConnectionStrings--Database" = "Server=PROD-SQL;Database=ErpDb;..." - "erp--Smtp--Password" = "smtp-passwort" - "portal--ConnectionStrings--Database" = "Server=PROD-SQL;Database=PortalDb;..." - "portal--Auth--JwtSecret" = "jwt-secret" - - # Non-Web Apps (OCORE) - "importer--ConnectionStrings--Database" = "Server=PROD-SQL;Database=ImportDb;..." - "scheduler--ConnectionStrings--Database" = "Server=PROD-SQL;Database=SchedDb;..." - "scheduler--Smtp--Password" = "smtp-passwort" - - # Weitere Apps ergänzen... -} - -$total = $allSecrets.Count -$done = 0 - -foreach ($name in $allSecrets.Keys) { - $value = ConvertTo-SecureString $allSecrets[$name] -AsPlainText -Force - Set-AzKeyVaultSecret -VaultName $VaultName -Name $name -SecretValue $value - $done++ - Write-Progress -Activity "Migriere Secrets" -Status "$name" -PercentComplete (($done/$total)*100) - Write-Host "✅ [$done/$total] $name" -} - -Write-Host "`nMigration abgeschlossen." -ForegroundColor Green -``` - -### 7.3 Bereinigung - -```powershell -# bereinigung.ps1 -$projektOrdner = @( - "C:\Projects\CRM", - "C:\Projects\ERP", - "C:\Projects\Portal", - "C:\Projects\Importer", - "C:\Projects\Scheduler" - # Alle Pfade eintragen -) - -foreach ($ordner in $projektOrdner) { - $file = Join-Path $ordner "appsettings.json" - if (Test-Path $file) { - $json = Get-Content $file -Raw | ConvertFrom-Json - if ($json.ConnectionStrings) { - $json.ConnectionStrings.PSObject.Properties | - ForEach-Object { $_.Value = "MANAGED_BY_KEYVAULT" } - } - $json | ConvertTo-Json -Depth 10 | Set-Content $file - Write-Host "✅ Bereinigt: $file" - } -} -``` - ---- - -## Teil 8: Server-Setup (einmalig) - -### 8.1 Managed Identity + Key Vault Zugriff - -```powershell -# server-setup.ps1 — einmalig als Admin - -# Managed Identity der VM abrufen -$identity = Get-AzVM -ResourceGroupName "MyRG" -Name "PROD-SERVER-01" -$objectId = $identity.Identity.PrincipalId - -# Key Vault: Server darf lesen, nicht schreiben -Set-AzKeyVaultAccessPolicy ` - -VaultName "company-vault" ` - -ObjectId $objectId ` - -PermissionsToSecrets Get, List - -Write-Host "✅ Key Vault Zugriff konfiguriert für Object ID: $objectId" -``` - -### 8.2 Schreibrechte für Cache-Dateien - -```powershell -# Für IIS Web Apps (App Pool User) -$webApps = @("CRM", "ERP", "Portal") -foreach ($pool in $webApps) { - $appPath = "C:\inetpub\$pool" - $user = "IIS AppPool\$pool" - $acl = Get-Acl $appPath - $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( - $user, "Modify", "ContainerInherit,ObjectInherit", "None", "Allow") - $acl.SetAccessRule($rule) - Set-Acl -Path $appPath -AclObject $acl - Write-Host "✅ Web App Schreibrecht: $user → $appPath" -} - -# Für Windows Services / Console Apps (LocalSystem oder dedizierter Service-User) -$serviceApps = @("Importer", "Scheduler") -foreach ($svc in $serviceApps) { - $appPath = "C:\services\$svc" - # LocalSystem hat standardmäßig Schreibrecht — nur prüfen ob Ordner existiert - if (-not (Test-Path $appPath)) { - New-Item -ItemType Directory -Path $appPath - Write-Host "✅ Service-Ordner erstellt: $appPath" - } -} -``` - ---- - -## Teil 9: .gitignore (Solution-Level) - -```gitignore -# Secret Management -secrets.cache -**/secrets.cache - -# Konfigurationsdateien mit echten Werten -appsettings.Production.json -appsettings.Staging.json - -# Zertifikate -*.pfx -*.p12 -*.key -``` - ---- - -## Teil 10: Pro-Projekt Checkliste - -### Web Apps (OCORE_web) - -``` -[ ] 1. AppName festlegen (kurz, eindeutig, Kleinbuchstaben) -[ ] 2. OCORE_web bereits referenziert (transitiv: OCORE kommt mit) -[ ] 3. Program.cs: - using OCORE_web.Secrets; - builder.AddSecretManagement(); -[ ] 4. appsettings.json: SecretManagement-Block mit korrektem AppName -[ ] 5. ManagedSecretKeys vollständig auflisten -[ ] 6. Secrets in Key Vault mit {appname}-- Präfix vorhanden -[ ] 7. Klartext-Secrets aus appsettings.json / web.config entfernt -[ ] 8. secrets.cache in .gitignore -[ ] 9. Logs nach Deploy prüfen: - "[OCORE.Secrets:crm] Sync OK" → ✅ - "[OCORE.Secrets:crm] Key Vault nicht erreichbar" → ❌ -``` - -### Non-Web Apps (OCORE) - -``` -[ ] 1. AppName festlegen (kurz, eindeutig, Kleinbuchstaben) -[ ] 2. OCORE bereits referenziert (vorhanden) -[ ] 3. Program.cs: - using OCORE.Secrets.Extensions; - Host.CreateDefaultBuilder(args).AddSecretManagement() - (Windows Service: zusätzlich .UseWindowsService()) -[ ] 4. appsettings.json: SecretManagement-Block mit korrektem AppName -[ ] 5. ManagedSecretKeys vollständig auflisten -[ ] 6. Secrets in Key Vault mit {appname}-- Präfix vorhanden -[ ] 7. Klartext-Secrets aus Konfigurationsdateien entfernt -[ ] 8. secrets.cache in .gitignore -[ ] 9. Logs prüfen nach erstem Start -``` - ---- - -## Zusammenfassung der Artefakte - -``` -OCORE/Secrets/ ← Einmalig anlegen - Models/AppSecretsOptions.cs - Cache/DpapiSecretsCache.cs - Cache/DpapiCacheConfigurationSource.cs - Sync/KeyVaultSyncService.cs - Extensions/PrefixKeyVaultSecretManager.cs - Extensions/SecretManagementExtensions.cs ← IHostBuilder + shared helpers - -OCORE_web/Secrets/ ← Einmalig anlegen (1 Datei) - SecretManagementWebExtensions.cs ← WebApplicationBuilder Extension - -Pro Web-Projekt (15x oder subset): - Program.cs → +2 Zeilen (using + builder.AddSecretManagement()) - appsettings.json → SecretManagement-Block, keine Secrets mehr - -Pro Non-Web-Projekt: - Program.cs → +2 Zeilen (using + .AddSecretManagement()) - appsettings.json → SecretManagement-Block, keine Secrets mehr - -Scripts/ - inventarisierung.ps1 ← Wo liegen aktuell Secrets? - migrate-all-to-keyvault.ps1 ← Einmalige Migration aller Apps - bereinigung.ps1 ← Platzhalter setzen nach Migration - server-setup.ps1 ← Managed Identity + ACLs (einmalig) -```