Merge branch 'phase/023-024' (Rotation, Widerruf)

This commit is contained in:
2026-10-12 13:31:47 +02:00
39 changed files with 3701 additions and 101 deletions
+32 -5
View File
@@ -1,19 +1,46 @@
# #023 Geführte Rotation
**Status:** offen
**Status:** erledigt
**Meilenstein:** v0.4.0
**Labels:** backend, ui
**Geschätzt:** 3 h **Tatsächlich:** –
**Geschätzt:** 3 h **Tatsächlich:** 0 h 20 min
## Ziel
Neuer Schlüssel an allen Verwendungen, Test, alter entfernt.
## Aufgaben
- [ ] wird zu Beginn der Phase verfeinert
- [x] „Rotieren“ auf der Schlüsselseite, Schrittfolge `/schluessel/[id]/rotieren` (wiederaufnehmbar)
- [x] Neuer Schlüssel gleichen Typs, Name „… (rotiert TT.MM.JJJJ)“, `vorgaengerId` = alter
- [x] Zuweisungen übernehmen: Public gleiche Ziele/Optionen, Private gleicher Host/Benutzer/Alias mit neuem Pfad
- [x] Dry-Run + Ausrollen, Verbindungstests, erst dann den alten widerrufen (#024) → `ROTIERT`
- [x] Personen-Key ohne Private Key: nur Hinweis „neuer Public Key nötig“
- [x] Rotationskette auf der Schlüsselseite (Vorgänger/Nachfolger verlinkt)
## Akzeptanzkriterien
- [ ] wird zu Beginn der Phase verfeinert
- [x] DB-Tests: Schritte, Reihenfolge, Rechte, Namen/Pfade (`tests/rotation.test.ts`)
- [x] Integrationstest: neuer Schlüssel funktioniert, alter von beiden Hosts weg, `ssh <alias>` geht weiter
- [x] E2E im Browser (`tests/e2e/rotation.spec.ts`), Screenshot `rotation-desktop.png`
## Abhängigkeiten
#020
#020, #024
## Notizen / Entscheidungen
- **Fünf Schritte:** Neuer Schlüssel → Zuweisungen übernehmen → Dry-Run & Ausrollen → Verbindungstests →
Alten widerrufen. Der aktuelle Schritt wird aus der Datenbank errechnet (`rotationsStand`), deshalb
lässt sich die Rotation jederzeit neu laden und fortsetzen; jeder Schritt ist idempotent.
- **Kette sofort:** Der Worker setzt `vorgaengerId` schon beim Erzeugen (neues Feld in der
Vorgangs-Eingabe) – auch ein abgebrochener Browser hinterlässt keinen verwaisten Schlüssel.
`vorgaengerId` ist eindeutig: zwei gleichzeitige Rotationen scheitern verständlich.
- **Gleicher Alias, zwei Zuweisungen:** `privat.ts` schreibt je Alias genau einen config-Eintrag, die
jüngste Zuweisung gewinnt (an der Position der ersten). So zeigt `ssh <alias>` nach dem Ausrollen auf
die neue Datei, während die alte bis zum Widerruf daneben liegt.
- **Neuer Pfad:** `<alter Pfad ohne -rJJJJMMTT>-rJJJJMMTT`, bei Kollision mit laufender Nummer –
auch entfernte Zuweisungen belegen einen Pfad (eindeutiger Index über alle Zustände).
- **Grün heißt aktuell:** Der alte Schlüssel wird nur widerrufen, wenn für jede Private-Zuweisung mit
Alias ein erfolgreicher `VERBINDUNGSTEST` nach dem letzten Ausrollen vorliegt (Server prüft das).
Ohne Private-Zuweisung mit Alias entfällt der Test.
- **Abschluss:** Widerruf aus #024 ohne Zielkonten (der Nachfolger nutzt sie weiter); ein aktiver
Nachfolger macht aus `WIDERRUFEN` den Status `ROTIERT`.
- Ablaufdatum: Der Nachfolger erbt es nur, wenn es in der Zukunft liegt. Eine Passphrase wird nicht
übernommen (sshkv erzeugt ohne).
- Rechte: `schluesselVerwalten` + `publicKeyAusrollen`, bei Private-Zuweisungen `privateKeyAusrollen`.
Deployment-Key: eigene Rotation (später).
+34 -4
View File
@@ -1,19 +1,49 @@
# #024 Widerruf und Massenwiderruf
**Status:** offen
**Status:** erledigt
**Meilenstein:** v0.4.0
**Labels:** backend, ui, sicherheit
**Geschätzt:** 3 h **Tatsächlich:** –
**Geschätzt:** 3 h **Tatsächlich:** 0 h 25 min
## Ziel
Ein-Klick-Widerruf inkl. Benutzer/sudoers; Massenwiderruf mit Re-Auth.
## Aufgaben
- [ ] wird zu Beginn der Phase verfeinert
- [x] „Widerrufen“ auf der Schlüsselseite mit Dialog, der jede Entfernung auflistet
- [x] Public- und Private-Verwendungen → ENTFERNEN, Dry-Run, anwenden, erst dann `WIDERRUFEN`
- [x] Zielkonten optional entfernen (nur `vonSshkvAngelegt`, nie ein noch genutztes)
- [x] Massenwiderruf auf der Schlüsselliste, ein gemeinsamer Dry-Run, Protokoll je Schlüssel
- [x] Abgelaufene Schlüssel erkennbar (Filter) und Widerruf anbieten – ohne automatisches Entfernen
## Akzeptanzkriterien
- [ ] wird zu Beginn der Phase verfeinert
- [x] DB-Tests: Zustände, Rechte, Zielkonten, Protokoll ohne Geheimnisse (`tests/widerruf.test.ts`)
- [x] Integrationstest gegen zwei Hosts inkl. „Host nicht erreichbar“ (`tests/integration/widerruf.test.ts`)
- [x] E2E im Browser (`tests/e2e/widerruf.spec.ts`), Screenshot `widerruf-desktop.png`
## Abhängigkeiten
#018
## Notizen / Entscheidungen
- **Zwischenzustand statt Sofort-Status:** neues Feld `Schluessel.widerrufEingeleitetAm` (Migration
`20261012130000_widerruf_eingeleitet`). Der Status bleibt AKTIV/ABGELAUFEN, bis keine Verwendung mehr
offen ist; erst `widerrufAbschliessen` setzt `WIDERRUFEN` (bzw. `ROTIERT`, wenn ein aktiver Nachfolger
existiert, #023). Schlüsselseite und Liste zeigen „Widerruf unvollständig“ und die offenen Ziele.
- **Reihenfolge:** vormerken → Dry-Run/Anwenden (bestehende Hash-Sperre aus #016/#017) → optional
Zielkonten (`ZIELKONTO_ENTFERNEN`, erst wenn die Schlüssel überall weg sind) → abschließen.
Ein Ziel, das im Dry-Run scheitert (nicht erreichbar), wird nicht angewendet; seine Verwendung bleibt
ENTFERNEN, ein neuer Dry-Run versucht es erneut. Der Abschluss läuft nach erfolgreichem Anwenden
automatisch und meldet, was noch fehlt.
- **Nichts zu schreiben:** Steht der Schlüssel schon nirgends mehr, bietet das Dry-Run-Panel
„Zustand übernehmen“ an (sonst bliebe der Widerruf hängen).
- **Archivierte Hosts:** sshkv verbindet sich dort nicht mehr – deren Verwendungen gelten sofort als
entfernt; der Dialog sagt das, das Protokoll nennt die Hosts.
- **Zielkonten:** Ein Zielkonto kennt seinen Schlüssel nicht; Kandidat ist ein von sshkv angelegtes Konto
auf (Host, Benutzer) einer Zuweisung der widerrufenen Schlüssel. Nutzt ein anderer, nicht im Widerruf
befindlicher Schlüssel das Konto noch, bleibt es (Dialog nennt den Schlüssel).
- **Rechte:** nur Public Keys → `publicKeyAusrollen`; Private Keys → `privateKeyAusrollen` (Admin);
mehrere Schlüssel → `massenwiderruf`, Zielkonten → `zielkontenVerwalten` (jeweils Admin mit frischer
Bestätigung). Der Deployment-Key ist nicht widerrufbar. Ein Schlüssel im Widerruf lässt sich nicht
mehr zuweisen.
- **Ablauf:** Status-Filter „abgelaufen“ umfasst aktive Schlüssel mit vergangenem Ablaufdatum; die
Schlüsselseite bietet dann „Jetzt widerrufen“. Entfernt wird nie automatisch.
- Massenwiderruf als GET-Formular `/schluessel/widerruf?id=…` – die Auswahl der Zielkonten reist als
`?zielkonten=` mit und übersteht Neuladen und erneute Bestätigung.