Secret-Provider

NodePilot verschlüsselt gespeicherte Machine-Credentials und als Secret markierte globale Variablen über ISecretProtector. Der aktive Provider bestimmt, ob verschlüsselte Werte auf eine Maschine begrenzt oder zwischen Cluster-Nodes übertragbar sind.

Aktuell stehen DPAPI und AES-GCM zur Verfügung. HashiCorp Vault Transit ist als zukünftiger Provider vorgesehen.

Provider-Matrix

ProviderSecrets:ProviderCluster-portableSetup
DPAPI (default)Dpapi (oder unset)❌ machine-boundCredentials:DpapiScope=LocalMachine für Produktion
AES-GCMAesGcm✅shared Secrets:MasterKey oder Secrets:MasterKeyFile (32-Byte base64) auf jedem Node
HashiCorp Vault Transit(noch nicht)✅Roadmap

Startup-Guardrails

  • Unbekannte Secrets:Provider-Werte (Typo AesGCMm) failen beim Boot — kein stiller Fallback auf DPAPI.
  • Cluster:Enabled=true + Secrets:Provider=Dpapi (oder default) failt beim Boot — DPAPI-Ciphertexte sind machine-bound, der Standby könnte sie nach Failover nicht entschlüsseln.

DPAPI

"Secrets": { "Provider": "Dpapi" },
"Credentials": { "DpapiScope": "LocalMachine" }

LocalMachine ist die Produktions-Empfehlung (überlebt Service-Account-Wechsel); CurrentUser (Dev-Default) re-bindet beim Account-Wechsel.

AES-GCM

"Secrets": {
  "Provider": "AesGcm",
  "MasterKeyFile": "C:\\ProgramData\\NodePilot\\secrets\\aesgcm-masterkey.txt"
}

Secrets:MasterKey bleibt fuer Env-Var-Deployments (Secrets__MasterKey) unterstuetzt. Fuer disk-backed Deployments Secrets:MasterKeyFile bevorzugen und die Datei auf die NodePilot-Service-Identitaet ACL-restricten.

Key generieren und auf jeden Cluster-Node kopieren:

$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
$bytes = New-Object byte[] 32
try {
    $rng.GetBytes($bytes)
    [Convert]::ToBase64String($bytes)
} finally {
    $rng.Dispose()
    [Array]::Clear($bytes, 0, $bytes.Length)
}
# oder
openssl rand -base64 32

Wire-Format: [1 byte version=0x01] [12 bytes nonce] [N bytes ciphertext] [16 bytes auth tag]. Der Versions-Byte ist der Hook für künftige Key-Rotation-Envelopes; heute nur 0x01.

Hardening: Der Master-Key muss AES-GCM verfuegbar sein, muss aber nicht in JSON liegen. Bevorzugt Secrets__MasterKey oder ACL-restricted Secrets:MasterKeyFile; JSON-backed Secrets:MasterKey nur mit restriktiven appsettings.Production.json-ACLs und ohne unverschluesselte Backups.

Migration DPAPI → AES-GCM

Schritt 1 — beide Provider verkabeln:

"Secrets": {
  "Provider": "AesGcm",                  // active: Writes, Reads zuerst hier
  "MasterKeyFile": "C:\\ProgramData\\NodePilot\\secrets\\aesgcm-masterkey.txt",
  "LegacyProvider": "Dpapi",             // Fallback für Rows noch im DPAPI-Format
  "LegacyDpapiScope": "LocalMachine"
}

Schritt 2 — Bulk-Re-Encrypt:

curl --proto "=https" --fail-with-body --show-error --silent \
     --write-out "\nHTTP %{http_code}\n" \
     -X POST -H "Authorization: Bearer <admin-token>" \
     https://nodepilot-vip/api/secrets/reencrypt
  • 200 OK → clean cutover (partialSuccess: false).
  • 207 Multi-Status → übersprungene Rows in *SkipDetails, manuell nachpflegen.

Der Sweep umfasst Credentials, Secret-Globals, Workflow-Versionen, Agent-MCP-Secrets, Benachrichtigungsrouten, wartende Ausführungsparameter und verschlüsselte Laufzeiteinstellungen einschließlich der .bak.*-Rollbackdateien des Writers. Für die drei letzten Bereiche meldet er notificationRoutesRewritten/Skipped, dispatchParametersRewritten/Skipped und runtimeSettingsFilesRewritten/Skipped sowie die jeweiligen singulären *SkipDetails-Arrays. Laufzeitzähler zählen Dateien; deren Skip-ID ist die leere GUID. Zwischenzeitlich geänderte Datenbankwerte ergeben ConcurrentUpdate, beschädigte Laufzeitdateien bleiben unverändert. Bereits erfolgreiche Umstellungen bleiben bei Teilfehlern oder Abbruch erhalten: Fallback beibehalten und den Sweep erneut ausführen.

Ein Wartungsfenster verwenden und Änderungen an Secrets/Einstellungen sowie Restore/Import pausieren. Zunächst alle Nodes mit Active+Legacy starten; der Fallback funktioniert bereits beim Laden verschlüsselter Laufzeiteinstellungen. In HA den Endpunkt auf jedem Node mit eigener Laufzeitdatei ausführen: Datenbankzeilen sind gemeinsam, das Dateiergebnis gilt nur für diesen Node. Externe exportierte Backups werden nicht umgeschrieben; deren Wiederherstellungsschlüssel aufbewahren oder die Backups ersetzen, bevor der alte Schlüssel endgültig entfällt.

Derselbe Sweep ist auch in der UI verfügbar — Admin-Einstellungen → Security → „Secrets neu verschlüsseln“ (Admin-only; Bestätigungsdialog, Ergebnis-Toast mit den Zählern, Partial Success als Fehler-Toast) — sowie per CLI: np secrets reencrypt.

Schritt 3 — Legacy-Config entfernen: erst wenn Step 2 200 mit partialSuccess=false liefert, alle Skip-Counter einschließlich workflowVersionsSkipped null sind und der nodepilot.credential.crypto.legacy_reads-Counter bei den Nachtests null bleibt. Bei einem History-Skip bleibt Secrets:LegacyProvider konfiguriert, bis die genannte Version wiederhergestellt/repariert und ein erneuter Sweep sauber ist. Danach Secrets:LegacyProvider/LegacyDpapiScope/LegacyMasterKey/LegacyMasterKeyFile entfernen und neu starten.

AES-GCM Master-Key rotieren

Gleiches Prozedere, aber LegacyProvider=AesGcm + LegacyMasterKey={{old-base64}} in Schritt 1. Schritt 2 + 3 unverändert.

API

EndpointAuthZweck
POST /api/secrets/reencryptAdminVerschlüsselt alle sieben oben genannten Datenbank-/Laufzeitbereiche neu. 200 (sauber) oder 207 (übersprungen, mit Details).

Audit: SECRETS_REENCRYPTED.

Out of scope (V1)

HashiCorp Vault Transit / Azure Key Vault / KMIP, HSM-backed Keys, Per-Row Key-ID, automatischer Background-Sweep. Die ISecretProtector-Schnittstelle ist so gebaut, dass ein netzwerkgestützter Provider in einer Klasse + DI-Zeile addierbar ist.

Alle Kapitel