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