# 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) ```