# #014 Private Key anzeigen und herunterladen **Status:** erledigt **Meilenstein:** v0.2.0 **Labels:** sicherheit **Geschätzt:** 2 h **Tatsächlich:** 0 h 10 min ## Ziel Nur Admin, nur nach Re-Auth, immer protokolliert; Entschlüsselung im Worker. ## Aufgaben - [x] Einmal-Abholung - [x] Option „Private Key nach Ausrollen löschen“ ## Akzeptanzkriterien - [x] Kein Caching, kein Logging ## Abhängigkeiten #012, #004 ## Notizen / Entscheidungen - **Ablauf:** Web legt einen Vorgang `PRIVATE_KEY_ABHOLEN` an (Protokoll `privatekey.angefordert`) → der Worker öffnet den Umschlag und legt den Key mit `SSHKV_APP_SECRET` verschlüsselt in `Vorgang.ergebnis` ab (`{ abholung: "v1.…" }`) → das Web holt ihn **genau einmal** ab: ein einziges `UPDATE … FROM (SELECT … FOR UPDATE) RETURNING` liest das alte Ergebnis und setzt `{ abgeholt: true }`; fünf gleichzeitige Abholungen ergeben genau eine (Test). Abholen darf nur, wer angefordert hat. Protokoll `privatekey.angezeigt` / `privatekey.heruntergeladen` mit Fingerprint, nie mit Key. - **Die Status-Schnittstelle** `/api/vorgaenge/[id]` zeigt für eine bereitliegende Abholung nur `{ bereit: true }`, nie das Chiffrat. - **Verfall nach 2 Minuten** (`ABHOLFRIST_MS` in `src/lib/schluessel.ts`), dreifach abgesichert: der Worker räumt alle 30 s und vor jeder neuen Abholung auf (`abholungenAufraeumen`, ein einziges UPDATE → `{ verfallen: true }`), und das Web gibt eine ältere Abholung nicht mehr heraus (überschreibt sie aber trotzdem). Ein Worker-Takt statt pg-boss-Zeitplan: dessen Raster ist eine Minute. - **Anzeige** im Dialog (Monospace, Kopier-Knopf, „Wird nicht erneut angezeigt“); der Key lebt nur im Zustand des Dialogs und wird beim Schließen verworfen. **Download** als Datei `` über `POST /api/private-key/[vorgangId]` (Formular, kein GET – ein Link oder Vorabruf würde die einmalige Abholung verbrauchen), `Content-Disposition: attachment`, `Cache-Control: no-store`. Der Route Handler prüft selbst Recht, Bestätigung und Herkunft: wegen `Referrer-Policy: no-referrer` senden Browser bei Formular-POSTs `Origin: null`, maßgeblich ist deshalb `Sec-Fetch-Site: same-origin` (oder ein passender Origin). - **Re-Auth:** Fehlt die frische Bestätigung, leitet die Oberfläche nach `/bestaetigen?ziel=/schluessel/?privatekey=1` und öffnet danach den Dialog wieder. Der Mock-OIDC liefert `auth_time` nur, wenn man es in die Claims schreibt – die E2E tut das. - **Löschen** („Private Key aus SSHKeyVault löschen“): neues Recht `privateKeyLoeschen` (nur Admin, mit Bestätigung), ausgeführt als Worker-Vorgang `PRIVATE_KEY_LOESCHEN` (`privateKeyVerwerfen` in `src/worker/geheimnisse.ts`), damit nur diese Datei die Chiffrat-Felder anfasst – auch zum Leeren. Setzt Chiffrat, DEK und Version auf `null` und `privatGeloeschtAm`; Protokoll `privatekey.geloescht`. CHECK `schluessel_geloescht_ohne_private_key` (Migration aus #012). - **Deployment-Keys** gibt sshkv weder heraus noch löscht es deren Private Key: der Key ist root auf allen Hosts, und ohne ihn käme der Worker nirgends mehr hin. Im Notfall wird rotiert.