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)
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“.
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.
Zusatzdatenschluessel:<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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
masterKeyLadenSSHKV_MASTER_KEY_FILEnur im Worker anbindencrypto), OpenSSH-Format schreiben und lesen (optional Passphrase: bcrypt-pbkdf + aes256-ctr)ssh-keygen -lschwaerzen; Einbau in Logger/Protokoll folgt)Akzeptanzkriterien
ssh-keygen -y -fliest erzeugte Schlüssel (mit und ohne Passphrase)ssh-keygen -lfAbhängigkeiten
#003
Notizen / Entscheidungen
ersten Start, Master-Key nur im Worker laden und der Nachweis „Datenbank-Dump enthält keinen
Klartext“.
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.openssh-key-v1stattssh-keygen: keine Private Keys als Temp-Dateien.Die Tests prüfen gegen das echte
ssh-keygen(beide Richtungen, mit/ohne Passphrase).aes256-ctr+bcryptmit 16 Runden (ssh-keygen nimmt heute 24).bcrypt-pbkdfist 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.comviassh-keygen -Z) werden mit klarer Meldungabgelehnt. Private-Key-Import nur für ed25519 und RSA.
SchluesselFormatFehlerträgt einencode(ungueltig,nicht_unterstuetzt,passphrase_fehlt,passphrase_falsch,zu_schwach); Meldungen sind feste Sätze ohne Eingabe.publicKeyZeileLesenundprivateKeyLesen;{ rsaMindestBits: 0 }für das reine Beobachten vorhandenerauthorized_keys(Drift).Zeile in
authorized_keyseinschleusen.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 zurVersion wählen.
SSHKV_MASTER_KEY_UNSICHER_ERLAUBT=1(dann Warnung). Achtung Docker: Secrets ohne Swarm sind Bind-Mounts mit den Rechten der
Host-Datei – dort
chmod 400und passenden Eigentümer setzen.privat,passwort,password,passphrase,secret,geheimnis,token,masterkey,authorization,cookieenthalten(nicht nur als Präfix), sowie auf PEM-Private-Keys in jeder Zeichenkette (auch abgeschnitten,
auch in
message/stackvon Fehlern). Rohe Bytes werden zu[Binärdaten].src/worker/geheimnisse.tsist die einzige Stelle, dieprivatVerschluesselt/dekVerschluesseltliest;tests/geheimnisse-grenze.test.tshält das strukturell fest, ebenso dass nichts außerhalb vonsrc/workervon dort importiert.schluessel:<id>:<fingerprint>: Die ID wird deshalb vor dem Versiegeln vergeben (randomUUID), nicht von Prisma.sshkv-deployment@<rechner>). Gleichzeitige Starts: der partielle Index lässt nur einen zu, der zweite liest den ersten.pg_dump --data-onlyder Testdatenbank enthält weder „PRIVATE KEY“ noch den Schlüsselkern.Quelle:
docs/issues/006-geheimnisse-schluesselformat.md– dort wird dieses Issue gepflegt.