Merge branch 'phase/012-014' (Schluessel, Personen, Private Key)
# Conflicts: # src/components/vorgang/useVorgang.ts # src/worker/handler.ts
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 152 KiB |
@@ -1,23 +1,60 @@
|
||||
# #012 Schlüssel erzeugen, importieren, exportieren
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.2.0
|
||||
**Labels:** backend, ui
|
||||
**Geschätzt:** 4 h **Tatsächlich:** –
|
||||
**Geschätzt:** 4 h **Tatsächlich:** 0 h 25 min
|
||||
|
||||
## Ziel
|
||||
Schlüsselliste und -formular: erzeugen (ed25519, rsa-4096), importieren (Public oder Paar), exportieren; Kategorien, Tags, Ablaufdatum.
|
||||
|
||||
## Aufgaben
|
||||
- [ ] Liste mit Filter
|
||||
- [ ] Erzeugen-Dialog
|
||||
- [ ] Import mit Prüfung
|
||||
- [ ] Export Public Key
|
||||
- [ ] Schlüsseldetail-Gerüst mit Verwendungen
|
||||
- [x] Liste mit Filter
|
||||
- [x] Erzeugen-Dialog
|
||||
- [x] Import mit Prüfung
|
||||
- [x] Export Public Key
|
||||
- [x] Schlüsseldetail-Gerüst mit Verwendungen
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- [ ] Private Keys erscheinen nie in Listen-Antworten
|
||||
- [x] Private Keys erscheinen nie in Listen-Antworten
|
||||
|
||||
## Abhängigkeiten
|
||||
#006
|
||||
|
||||
## Notizen / Entscheidungen
|
||||
- **Migration `20261012100000_schluessel_vorgaenge`:** Vorgangsarten `SCHLUESSEL_ERZEUGEN`,
|
||||
`SCHLUESSEL_IMPORTIEREN`, `PRIVATE_KEY_LOESCHEN` (für #014 gleich mit), CHECK
|
||||
`schluessel_slug_gueltig` (`^[a-z0-9-]{1,64}$`, der Slug landet in Dateinamen) und
|
||||
`schluessel_geloescht_ohne_private_key`.
|
||||
- **Erzeugen und Paar-Import laufen im Worker** (`src/worker/ablaeufe/schluessel.ts`). Das Web
|
||||
prüft nur Metadaten (Slug frei, Person/Vorlage vorhanden, Ablauf nicht in der Vergangenheit)
|
||||
und reiht den Vorgang ein. Passphrase und importierter Private Key reisen wie das
|
||||
Bootstrap-Passwort mit `SSHKV_APP_SECRET` verschlüsselt im Job (`geheim`), nie in
|
||||
`Vorgang.eingabe`; der Worker löscht den Job danach. Der Worker prüft die Metadaten erneut.
|
||||
- **Kategorie:** Erzeugen und Paar-Import legen immer `DIENST` an. Personen-Keys speichert sshkv
|
||||
nie mit Private Key (DB-CHECK), Deployment-Keys verwaltet der Worker selbst. Der
|
||||
Public-Key-Import kennt `PERSON` und `DIENST`.
|
||||
- **Passphrase:** beim Erzeugen optional; gespeichert wird dann der passphrasengeschützte
|
||||
OpenSSH-Key (`passphraseGeschuetzt = true`). Beim Import dient sie nur zum Lesen von Public Key
|
||||
und Fingerprint und wird verworfen; gespeichert wird der Key so, wie er kam. Hinweis in der
|
||||
Oberfläche: „Für automatische Anmeldungen (Host-zu-Host) ungeeignet.“
|
||||
- **Typ:** `RSA4096` nur bei genau 4096 Bit, sonst `ANDERER` (z. B. RSA-3072, ecdsa). Die RSA-
|
||||
Mindestlänge 2048 gilt beim Import.
|
||||
- **Slug:** aus dem Zweck abgeleitet (Umlaute gefaltet), bei Kollision `-2`, `-3` …; ein
|
||||
ausdrücklich gewünschter Kurzname muss frei sein. Slug, Typ und Kommentar sind danach nicht
|
||||
mehr änderbar – sie stehen schon in Dateinamen und authorized_keys-Zeilen.
|
||||
- **Status von Hand** nur `AKTIV` ↔ `ABGELAUFEN`; `ROTIERT`/`WIDERRUFEN` setzen #023/#024,
|
||||
Deployment-Keys sind ausgenommen (sonst erzeugte der Worker beim Start einen neuen).
|
||||
- **Ablaufdatum** ohne Angabe aus `Vorlage.standardLaufzeitTage`.
|
||||
- **„Private Key gespeichert?“** liest das Web aus `masterKeyVersion` (der CHECK
|
||||
`schluessel_umschlag_vollstaendig` koppelt sie an das Chiffrat) – das Chiffrat wird im Web
|
||||
nie selektiert.
|
||||
- **Grenze abgesichert:** `tests/schluessel-grenze.test.ts` verlangt für jeden zeilenliefernden
|
||||
`prisma.schluessel.*`-Aufruf außerhalb von `src/worker` ein `select` und verbietet
|
||||
`schluessel: true` als Relation; `tests/schluessel.test.ts` prüft, dass weder Vorgang,
|
||||
Protokoll noch pg-boss-Job nach Erzeugen/Import den Key oder die Passphrase enthalten.
|
||||
- **Platzhalter** für #016/#017: `src/app/(app)/schluessel/[id]/PublicAusrollenAktion.tsx` und
|
||||
`PrivateAusrollenAktion.tsx`, Props `{ schluesselId: string; darf: boolean }`, rendern `null`.
|
||||
- Personen-Keys zeigen im Detail nur die Karte „Public Key ausgerollt auf“ (eine Private-Karte
|
||||
wäre immer leer).
|
||||
- Prototyp `/schluessel/beispiel` samt `src/beispiel/schluessel.ts` entfernt; die Übersicht
|
||||
(noch Prototyp, #026) verlinkt auf `/schluessel`.
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
# #013 Personen und Self-Service
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.2.0
|
||||
**Labels:** backend, ui
|
||||
**Geschätzt:** 2 h **Tatsächlich:** –
|
||||
**Geschätzt:** 2 h **Tatsächlich:** 0 h 10 min
|
||||
|
||||
## Ziel
|
||||
Personen verwalten; jeder angemeldete Benutzer kann eigene Public Keys hinterlegen, wirksam erst nach Zuweisung.
|
||||
|
||||
## Aufgaben
|
||||
- [ ] Personenliste/-detail
|
||||
- [ ] Seite „Meine Schlüssel“
|
||||
- [x] Personenliste/-detail
|
||||
- [x] Seite „Meine Schlüssel“
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- [ ] Leser kann nur eigene Keys hinterlegen, nicht zuweisen
|
||||
- [x] Leser kann nur eigene Keys hinterlegen, nicht zuweisen
|
||||
|
||||
## Abhängigkeiten
|
||||
#012
|
||||
|
||||
## Notizen / Entscheidungen
|
||||
- **Personen pflegt, wer Schlüssel verwaltet** (Recht `schluesselVerwalten`, Admin und Operator) –
|
||||
ein eigenes Recht wäre nur ein Alias. Lesen dürfen alle Rollen.
|
||||
- **Archivieren statt Löschen:** die Schlüssel bleiben der Person zugeordnet (wer hatte bis wann
|
||||
welchen Key?), die Kontoverknüpfung entfällt, damit der Self-Service nicht bei einer
|
||||
archivierten Person landet. Die Notiz steht nicht im Protokoll, nur dass sie sich änderte.
|
||||
- **Self-Service `/meine-schluessel`** (Recht `eigeneSchluesselHinterlegen`, alle Rollen,
|
||||
Navigation „Meine Schlüssel“): beim ersten Aufruf wird die eigene Person angelegt oder
|
||||
verknüpft – per `benutzerId`, sonst per E-Mail, aber **nur bei vom Anbieter bestätigter
|
||||
Adresse** und genau einer freien, nicht archivierten Person. Dafür neue Spalte
|
||||
`Benutzer.emailBestaetigt` (Migration `20261012103000_benutzer_email_bestaetigt`), gesetzt bei
|
||||
jeder OIDC-Anmeldung. Ohne sie könnte jemand mit selbst gewählter Adresse die Keys einer fremden
|
||||
Person sehen.
|
||||
- Hinterlegte Keys: Kategorie `PERSON`, `selbstHinterlegt = true`, Protokoll
|
||||
`schluessel.selbst_hinterlegt`. Hinweis in der Oberfläche: „Wirksam erst, wenn ein Operator
|
||||
oder Admin den Schlüssel einem Host zuweist.“
|
||||
- **Entfernen** nur eigene, selbst hinterlegte Keys ohne jede Verwendung (auch keine entfernte –
|
||||
der Verlauf am Key bleibt). Das Löschen ist bedingt (`deleteMany` mit „keine Verwendungen“),
|
||||
damit eine gleichzeitige Zuweisung nicht per Cascade verschwindet. Fremde IDs gelten als „nicht
|
||||
vorhanden“. Tests: `tests/personen.test.ts`.
|
||||
- Die Services nehmen den angemeldeten Benutzer entgegen und leiten die Person selbst ab; die
|
||||
Actions reichen keine `personId` von außen durch (struktureller Test).
|
||||
|
||||
@@ -1,20 +1,52 @@
|
||||
# #014 Private Key anzeigen und herunterladen
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.2.0
|
||||
**Labels:** sicherheit
|
||||
**Geschätzt:** 2 h **Tatsächlich:** –
|
||||
**Geschätzt:** 2 h **Tatsächlich:** 0 h 10 min
|
||||
|
||||
## Ziel
|
||||
Nur Admin, nur nach Re-Auth, immer protokolliert; Entschlüsselung im Worker.
|
||||
|
||||
## Aufgaben
|
||||
- [ ] Einmal-Abholung
|
||||
- [ ] Option „Private Key nach Ausrollen löschen“
|
||||
- [x] Einmal-Abholung
|
||||
- [x] Option „Private Key nach Ausrollen löschen“
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- [ ] Kein Caching, kein Logging
|
||||
- [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 `<slug>`
|
||||
ü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/<id>?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.
|
||||
|
||||