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

Closed
opened 2026-10-12 15:12:35 +00:00 by florian.wilde · 0 comments
Owner

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.

Quelle: docs/issues/006-geheimnisse-schluesselformat.md – dort wird dieses Issue gepflegt.

## 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. --- Quelle: `docs/issues/006-geheimnisse-schluesselformat.md` – dort wird dieses Issue gepflegt.
florian.wilde added this to the v0.1.0 milestone 2026-10-12 15:12:35 +00:00
florian.wilde added the sicherheit label 2026-10-12 15:12:35 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: florian.wilde/sshkeyvault#6