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

62 lines
4.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# #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
- [x] Master-Key aus Datei laden (32 Byte roh, Base64 oder Hex; Rechteprüfung) – `masterKeyLaden`
- [x] Master-Key aus `SSHKV_MASTER_KEY_FILE` nur im Worker anbinden
- [x] AES-256-GCM, DEK je Schlüssel, AAD = Schlüssel-ID + Fingerprint, Master-Key-Version
- [x] ed25519 und RSA-4096 erzeugen (Node `crypto`), OpenSSH-Format schreiben und lesen (optional Passphrase: bcrypt-pbkdf + aes256-ctr)
- [x] Fingerprint SHA256 wie `ssh-keygen -l`
- [x] Deployment-Key beim ersten Start erzeugen
- [x] Log-Redaction (`schwaerzen`; Einbau in Logger/Protokoll folgt)
## Akzeptanzkriterien
- [x] `ssh-keygen -y -f` liest erzeugte Schlüssel (mit und ohne Passphrase)
- [x] Fingerprints identisch zu `ssh-keygen -lf`
- [x] 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.