Files
sshkeyvault/docs/issues/006-geheimnisse-schluesselformat.md
florian.wildeandClaude Opus 5.5 9b807c13ae feat(worker): Private Keys im Worker versiegeln, Deployment-Key (#006)
Der zweite Teil von #006 bindet den Kern an Datenbank und Worker. Nur
src/worker/geheimnisse.ts liest Chiffrat und Data-Key; ein struktureller
Test haelt fest, dass der Webprozess nichts von dort importiert und so
nie an den Master-Key kommt.

Die Zusatzdaten binden jedes Chiffrat an ID und Fingerprint seines
Schluessels: kopiert man es in eine andere Zeile, laesst es sich dort
nicht oeffnen. Der Deployment-Key entsteht beim ersten Start; ein Test
prueft, dass ein pg_dump keinen Klartext enthaelt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:33:18 +02:00

4.3 KiB
Raw Permalink Blame History

#006 Verschlüsselte Ablage und OpenSSH-Schlüsselformat

Status: erledigt Meilenstein: v0.1.0 Labels: sicherheit Geschätzt: 4 h Tatsächlich: 0 h 25 min

Ziel

Envelope Encryption für Private Keys mit Master-Key aus Datei; eigene Erzeugung und Serialisierung von OpenSSH-Schlüsseln ohne temporäre Dateien; Deployment-Key der Instanz.

Aufgaben

  • Master-Key aus Datei laden (32 Byte roh, Base64 oder Hex; Rechteprüfung) – masterKeyLaden
  • Master-Key aus SSHKV_MASTER_KEY_FILE nur im Worker anbinden
  • AES-256-GCM, DEK je Schlüssel, AAD = Schlüssel-ID + Fingerprint, Master-Key-Version
  • ed25519 und RSA-4096 erzeugen (Node crypto), OpenSSH-Format schreiben und lesen (optional Passphrase: bcrypt-pbkdf + aes256-ctr)
  • Fingerprint SHA256 wie ssh-keygen -l
  • Deployment-Key beim ersten Start erzeugen
  • Log-Redaction (schwaerzen; Einbau in Logger/Protokoll folgt)

Akzeptanzkriterien

  • ssh-keygen -y -f liest erzeugte Schlüssel (mit und ohne Passphrase)
  • Fingerprints identisch zu ssh-keygen -lf
  • Datenbank-Dump enthält keinen Klartext-Schlüssel

Abhängigkeiten

#003

Notizen / Entscheidungen

  • Kern fertig; Anbindung an Datenbank/Worker folgt mit #007. Offen sind Deployment-Key beim ersten Start, Master-Key nur im Worker laden und der Nachweis „Datenbank-Dump enthält keinen Klartext“.
  • Module: src/lib/ssh/openssh.ts (Erzeugen/Schreiben/Lesen, Fingerprint, Public-Key-Zeilen), src/lib/krypto/umschlag.ts (Envelope Encryption, Master-Key laden), src/lib/protokoll-schwaerzen.ts (Redaction). Alle framework-frei.
  • Eigener Serializer für openssh-key-v1 statt ssh-keygen: keine Private Keys als Temp-Dateien. Die Tests prüfen gegen das echte ssh-keygen (beide Richtungen, mit/ohne Passphrase).
  • Passphrase: aes256-ctr + bcrypt mit 16 Runden (ssh-keygen nimmt heute 24). bcrypt-pbkdf ist reines JavaScript; 16 Runden halten das im Worker unter einer Sekunde. Beim Lesen gilt die Rundenzahl aus der Datei (Obergrenze 1000 gegen absichtlich teure Importe). Andere Cipher (z. B. aes256-gcm@openssh.com via ssh-keygen -Z) werden mit klarer Meldung abgelehnt. Private-Key-Import nur für ed25519 und RSA.
  • SchluesselFormatFehler trägt einen code (ungueltig, nicht_unterstuetzt, passphrase_fehlt, passphrase_falsch, zu_schwach); Meldungen sind feste Sätze ohne Eingabe.
  • RSA-Mindestlänge 2048 Bit standardmäßig in publicKeyZeileLesen und privateKeyLesen; { rsaMindestBits: 0 } für das reine Beobachten vorhandener authorized_keys (Drift).
  • Kommentare mit Steuerzeichen werden abgelehnt, sonst ließe sich über einen Kommentar eine zweite Zeile in authorized_keys einschleusen.
  • Umschlag-Format je Puffer: Version (1) | IV (12) | Tag (16) | Daten. AAD Klartext = zusatz (vorgesehen: schluessel:<id>:<fingerprint>), AAD DEK = sshkv-dek:v<masterKeyVersion>. versiegeln(..., masterKeyVersion = 1); beim Öffnen muss der Aufrufer den Master-Key passend zur Version wählen.
  • Master-Key-Datei mit Gruppe/Welt-Leserecht → Abbruch, außer SSHKV_MASTER_KEY_UNSICHER_ERLAUBT=1 (dann Warnung). Achtung Docker: Secrets ohne Swarm sind Bind-Mounts mit den Rechten der Host-Datei – dort chmod 400 und passenden Eigentümer setzen.
  • Schwärzen greift auf Feldnamen, die einen der Wortstämme privat, passwort, password, passphrase, secret, geheimnis, token, masterkey, authorization, cookie enthalten (nicht nur als Präfix), sowie auf PEM-Private-Keys in jeder Zeichenkette (auch abgeschnitten, auch in message/stack von Fehlern). Rohe Bytes werden zu [Binärdaten].
  • Anbindung im Worker (Teil 2): src/worker/geheimnisse.ts ist die einzige Stelle, die privatVerschluesselt/dekVerschluesselt liest; tests/geheimnisse-grenze.test.ts hält das strukturell fest, ebenso dass nichts außerhalb von src/worker von dort importiert.
  • Zusatzdaten schluessel:<id>:<fingerprint>: Die ID wird deshalb vor dem Versiegeln vergeben (randomUUID), nicht von Prisma.
  • Deployment-Key entsteht beim Start des Workers (ed25519, Kommentar sshkv-deployment@<rechner>). Gleichzeitige Starts: der partielle Index lässt nur einen zu, der zweite liest den ersten.
  • Dump-Test: pg_dump --data-only der Testdatenbank enthält weder „PRIVATE KEY“ noch den Schlüsselkern.