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
| Provider | Secrets:Provider | Cluster-portable | Setup |
|---|---|---|---|
| DPAPI (default) | Dpapi (oder unset) | ❌ machine-bound | Credentials:DpapiScope=LocalMachine für Produktion |
| AES-GCM | AesGcm | ✅ | shared Secrets:MasterKey oder Secrets:MasterKeyFile (32-Byte base64) auf jedem Node |
| HashiCorp Vault Transit | (noch nicht) | ✅ | Roadmap |
Startup-Guardrails
- Unbekannte
Secrets:Provider-Werte (TypoAesGCMm) 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
| Endpoint | Auth | Zweck |
|---|---|---|
POST /api/secrets/reencrypt | Admin | Verschlü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.