Add Azure Key Vault + DPAPI secret management
Playwright Tests / test (push) Waiting to run

Integrate OCORE/OCORE_web-based secret management using Azure Key Vault and DPAPI cache. Update appsettings.json to remove plaintext secrets and list managed keys. Register secret management in Program.cs. Update .gitignore for secret files. Add documentation for naming conventions and migration, plus a PowerShell script for initial secret upload. Centralizes and secures secret handling across the app.
This commit is contained in:
2026-05-03 16:24:38 +02:00
parent c617e9ae3b
commit cc2abc91d6
6 changed files with 1116 additions and 9 deletions
@@ -0,0 +1,933 @@
# 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";
/// <summary>
/// Vollständige URI des Azure Key Vault.
/// Beispiel: https://company-vault.vault.azure.net/
/// </summary>
public string VaultUri { get; set; } = string.Empty;
/// <summary>
/// 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).
/// </summary>
public string AppName { get; set; } = string.Empty;
/// <summary>
/// Pfad zur DPAPI-Cache-Datei (relativ zum App-Verzeichnis).
/// </summary>
public string CacheFilePath { get; set; } = "secrets.cache";
/// <summary>
/// Sync-Intervall in Stunden.
/// </summary>
public int SyncIntervalHours { get; set; } = 6;
/// <summary>
/// Secret-Namen ohne App-Präfix.
/// Beispiel: ["ConnectionStrings--Database", "ExternalApi--ApiKey"]
/// </summary>
public List<string> 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<string, string> secrets);
Task<Dictionary<string, string>> ReadAsync();
bool Exists();
}
public class DpapiSecretsCache : IDpapiSecretsCache
{
private readonly string _filePath;
private readonly byte[] _entropy;
private readonly ILogger<DpapiSecretsCache> _logger;
/// <summary>
/// Entropy wird aus AppName abgeleitet — verhindert dass eine andere App
/// auf demselben Server den Cache dieser App lesen kann.
/// </summary>
public DpapiSecretsCache(string filePath, string appName, ILogger<DpapiSecretsCache> logger)
{
_filePath = filePath;
_entropy = Encoding.UTF8.GetBytes($"dpapi-entropy-{appName}");
_logger = logger;
}
public async Task WriteAsync(Dictionary<string, string> 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<Dictionary<string, string>> ReadAsync()
{
if (!Exists()) return [];
try
{
var encrypted = await File.ReadAllBytesAsync(_filePath);
var decrypted = ProtectedData.Unprotect(encrypted, _entropy, DataProtectionScope.LocalMachine);
return JsonSerializer.Deserialize<Dictionary<string, string>>(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<DpapiSecretsCache>();
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;
/// <summary>
/// Läuft als BackgroundService in jedem Host-Typ:
/// Web Apps, Console Apps, Windows Services, Worker Services.
/// Synct Secrets aus Key Vault → DPAPI-Cache, periodisch.
/// </summary>
public class KeyVaultSyncService : BackgroundService
{
private readonly SecretClient _secretClient;
private readonly IDpapiSecretsCache _cache;
private readonly IConfigurationRoot _configRoot;
private readonly AppSecretsOptions _options;
private readonly ILogger<KeyVaultSyncService> _logger;
public KeyVaultSyncService(
SecretClient secretClient,
IDpapiSecretsCache cache,
IConfiguration configuration,
IOptions<AppSecretsOptions> options,
ILogger<KeyVaultSyncService> 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<string, string>();
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;
/// <summary>
/// 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"
/// </summary>
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
{
/// <summary>
/// Für Console Apps, Worker Services, Windows Services, WinForms/WPF.
/// Verwendung: Host.CreateDefaultBuilder(args).AddSecretManagement()
/// </summary>
public static IHostBuilder AddSecretManagement(this IHostBuilder hostBuilder)
{
hostBuilder.ConfigureAppConfiguration((context, config) =>
{
var built = config.Build();
var options = built.GetSection(AppSecretsOptions.SectionName).Get<AppSecretsOptions>()
?? 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;
}
/// <summary>
/// Gemeinsame Service-Registrierung — wird von OCORE und OCORE_web verwendet.
/// </summary>
public static void RegisterCoreServices(IServiceCollection services, IConfiguration configuration)
{
var options = configuration
.GetSection(AppSecretsOptions.SectionName)
.Get<AppSecretsOptions>() ?? new AppSecretsOptions();
if (!IsConfigured(options)) return;
services.Configure<AppSecretsOptions>(
configuration.GetSection(AppSecretsOptions.SectionName));
services.AddSingleton(new SecretClient(
new Uri(options.VaultUri),
new DefaultAzureCredential()));
services.AddSingleton<IDpapiSecretsCache>(sp =>
new DpapiSecretsCache(
ResolveCachePath(AppContext.BaseDirectory, options.CacheFilePath),
options.AppName,
sp.GetRequiredService<ILogger<DpapiSecretsCache>>()));
services.AddHostedService<KeyVaultSyncService>();
}
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
{
/// <summary>
/// Für ASP.NET Core Web Apps und Web APIs.
/// Verwendung: builder.AddSecretManagement()
/// </summary>
public static WebApplicationBuilder AddSecretManagement(
this WebApplicationBuilder builder)
{
var options = builder.Configuration
.GetSection(AppSecretsOptions.SectionName)
.Get<AppSecretsOptions>() ?? 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<MyWorker>();
})
.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<MyWindowsService>();
})
.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<MainForm>();
})
.Build();
Application.Run(host.Services.GetRequiredService<MainForm>());
```
---
## 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)
```
+4
View File
@@ -1,5 +1,6 @@
using Fuchs.intranet;
using Fuchs.Logging;
using OCORE_web.Secrets;
using Fuchs.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.StaticFiles;
@@ -28,6 +29,9 @@ public class Program
.SetMinimumLevel(LogLevel.Debug)
.AddFuchsLogging();
// Key Vault + DPAPI secret management (must run before FuchsOcmsIntranet.Initialize)
builder.AddSecretManagement();
// Initialize the Fuchs intranet singleton with configuration
FuchsOcmsIntranet.Initialize(builder.Configuration);
+24 -8
View File
@@ -1,4 +1,20 @@
{
"SecretManagement": {
"VaultUri": "https://pcwkeys.vault.azure.net/",
"AppName": "fuchs",
"CacheFilePath": "secrets.cache",
"SyncIntervalHours": 6,
"ManagedSecretKeys": [
"ConnectionStrings--ocms-ConnectionString",
"ConnectionStrings--fuchs-fds-ConnectionString",
"Fuchs--SMS-APIKey",
"Fuchs--Email--Main--password",
"Fuchs--Email--Fds--password",
"Fuchs--Email--Service--password",
"Fuchs--fuchs-captcha-TOTP",
"Fuchs--fuchs-intranet-TOTP"
]
},
"Logging": {
"LogLevel": {
"Default": "Information",
@@ -7,8 +23,8 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"ocms_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_web;password='Bt5pL/cJg9oxb5';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;",
"fuchs_fds_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_dev;password='!Po@cGZ5bUn37khO';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;"
"ocms_ConnectionString": "MANAGED_BY_KEYVAULT",
"fuchs_fds_ConnectionString": "MANAGED_BY_KEYVAULT"
},
"Fuchs": {
"ocms_guid": "00094b8f-a822-4e9c-b627-87802f93fca8",
@@ -16,9 +32,9 @@
"ocms_default_locale": "de",
"fuchs_guid": "cbfc57b3-6b85-4bbc-ab68-3b2c7408af5e",
"fuchs_intranet_guid": "cbfc57b3-6b85-4bbc-ab68-3b2c7408af5e",
"fuchs_captcha_TOTP": "4OXKGB3KS3VZNIUTTQLHECRUVN7ZDEFGSXYVU56D7UCKQZK7VHK7ZN",
"fuchs_intranet_TOTP": "ZNQIUF4KC5XSL2ZXK6VQIZYG74SAMW7FDAGT7ZOVYFJCXBJ47RQW3O",
"SMS_APIKey": "VLbm04ILlDby4EHjqolI9L95bAnfsipJcli0uvppMBHVq0BI1YR2gvpbKJRWDINu",
"fuchs_captcha_TOTP": "MANAGED_BY_KEYVAULT",
"fuchs_intranet_TOTP": "MANAGED_BY_KEYVAULT",
"SMS_APIKey": "MANAGED_BY_KEYVAULT",
"Email": {
"Main": {
"alias": "Sebastian Fuchs - Bad und Heizung",
@@ -29,7 +45,7 @@
"port": 587,
"security": "StartTls",
"username": "anfrage@sanitaerfuchs.de",
"password": "DsCG8wxc4!Cu9"
"password": "MANAGED_BY_KEYVAULT"
},
"Fds": {
"alias": "Sebastian Fuchs - Bad und Heizung",
@@ -40,7 +56,7 @@
"port": 587,
"security": "StartTls",
"username": "rechnungen@sanitaerfuchs.de",
"password": "8M9#s7TVg6b"
"password": "MANAGED_BY_KEYVAULT"
},
"Service": {
"alias": "ProcessWeb Service",
@@ -51,7 +67,7 @@
"port": 587,
"security": "StartTls",
"username": "service@emails.processweb.de",
"password": "Uk84za4Qzba4ij"
"password": "MANAGED_BY_KEYVAULT"
},
"TestAddresses": "st.ott@web.de,info@processweb.de"
}