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>
62 lines
4.3 KiB
Markdown
62 lines
4.3 KiB
Markdown
# #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.
|