Files
sshkeyvault/DEVLOG.md
T
florian.wildeandClaude Opus 5.5 7e4968f2ea docs(devlog): Sitzung 8 mit #035 und #036 (#036)
Zeiten und Entscheidungen der Sitzung festhalten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:52:52 +02:00

205 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DEVLOG – SSHKeyVault
## Zeitübersicht
**Gesamt (Wandzeit):** 6 h 40 min
**Summe der Issue-Zeiten:** 16 h 38 min – Sub-Agenten arbeiteten parallel an #002, #006, #008, #011, #012–#014, #019 (Oberfläche), #023–#025, #027, #028 und #032.
| Issue | Zeit |
|---|---|
| Planung (Phase 0) | 0 h 20 min |
| #001 Projekt-Setup | 0 h 15 min |
| #002 Design-System und Layout | 0 h 18 min |
| #003 Datenmodell und Migrationen | 0 h 10 min |
| #004 Anmeldung, Rollen und Bestätigung | 0 h 45 min |
| #005 Revisionssicheres Protokoll | 0 h 10 min |
| #006 Verschlüsselte Ablage und Schlüsselformat | 0 h 25 min |
| #007 Worker und Vorgänge | 0 h 15 min |
| #008 Hosts und Hostgruppen | 0 h 30 min |
| #009 Host-Key-Prüfung | 0 h 25 min |
| #010 Bootstrap | 0 h 35 min |
| #011 Test-SSH-Container | 0 h 15 min |
| #012 Schlüssel erzeugen, importieren, exportieren | 0 h 25 min |
| #013 Personen und Self-Service | 0 h 10 min |
| #014 Private Key anzeigen | 0 h 10 min |
| #015 authorized_keys-Parser | 0 h 15 min |
| #016 Public Key ausrollen | 0 h 40 min |
| #017 Private Key ausrollen | 0 h 45 min |
| #018 Zielbenutzer und sudoers | 0 h 30 min |
| #019 Vorlagen | 0 h 20 min |
| #020 Host-zu-Host-Assistent | 0 h 50 min |
| #021 Abgleich und Drift-Erkennung | 0 h 45 min |
| #022 Import bestehender Schlüssel | 0 h 20 min |
| #023 Geführte Rotation | 0 h 20 min |
| #024 Widerruf und Massenwiderruf | 0 h 25 min |
| #025 Zuweisungsmatrix | 0 h 30 min |
| #026 Übersicht | 0 h 25 min |
| #027 Protokollansicht, CSV, Syslog | 0 h 20 min |
| #028 Benachrichtigungen | 0 h 30 min |
| #029 Handbuch und Betriebsdokumentation | 0 h 50 min |
| #030 Gitea-Abgleich | 0 h 05 min |
| #031 Rotation des Deployment-Keys | 1 h 10 min |
| #032 Nacharbeiten Phase 4/5 | 0 h 45 min |
| #033 Erneut rotieren nach Abbruch | 0 h 20 min |
| #034 Abnahme-Checkliste | 0 h 10 min |
| #035 Lokale Benutzerkonten | 0 h 35 min |
| #036 Deployment-Key-Rotation abbrechen | 0 h 20 min |
| Meilenstein | Zeit |
|---|---|
| v0.1.0 | 1 h 25 min (Wandzeit) | **abgeschlossen**
| v0.2.0 | 1 h 00 min (Wandzeit) | **abgeschlossen**
| v0.3.0 | 0 h 30 min (Wandzeit) | **abgeschlossen**
| v0.4.0 | 0 h 45 min (Wandzeit) | **abgeschlossen**
| v0.5.0 | 0 h 45 min (Wandzeit) | **abgeschlossen**
| v1.0.0 | 2 h 15 min (Wandzeit) | in Arbeit – Abnahme steht aus
---
## 2026-10-12 · Sitzung 1 · 08:45–10:10
**Issue:** Planung, #001–#011
**Version:** 0.1.0
### Erledigt
- Auftrag gelesen, Architektur, Datenmodell, Sicherheitskonzept und Phasenplan entworfen (`docs/PLAN.md`)
- Sieben Rückfragen gestellt und beantwortet, Plan freigegeben
- Konventionen aus Laufzettel und ProxLedger übernommen; Git-Repository mit lokaler Identität angelegt
- 30 Issues für Phase 1–5 als Dateien angelegt (Gitea folgt mit #030)
- #001 Grundgerüst; #003 Datenmodell mit Datenbankregeln und Protokoll-Hash-Kette als Trigger
- #002, #006 (Kern), #011 parallel von Sub-Agenten in eigenen Worktrees, danach gemergt
- #004 Anmeldung: OIDC gegen Mock-Anbieter, Notfall-Admin mit TOTP, Re-Auth; E2E im gebauten Stand
- #005 Protokoll-Service mit Schwärzen; #006 Teil 2: Versiegeln im Worker, Deployment-Key
- #007 Worker mit pg-boss; vollständiger Docker-Stapel geprüft (migrate, web, worker gesund)
- #008 Hosts und Hostgruppen (Sub-Agent)
- #009 strikte Host-Key-Prüfung und TOFU, #010 Bootstrap – Integrationstests gegen echtes sshd, E2E im Browser bis zur Verbindungsprüfung
- Stand: 287 Unit-/DB-Tests, 17 Integrationstests, 5 E2E-Abläufe grün
### Entscheidungen
- **Kein Auth.js:** v5 steht dauerhaft auf Beta – wie in Laufzettel `openid-client` + `jose`.
- **Web und Worker getrennt, Master-Key nur im Worker:** Eine Lücke im Webprozess allein entschlüsselt keine Private Keys.
- **Schlüsselerzeugung ohne `ssh-keygen`:** Node `crypto` + eigener OpenSSH-Serializer, damit Private Keys nie als Datei auf dem sshkv-Server liegen. `ssh-keygen` prüft in den Tests die Kompatibilität.
- **Dienstbenutzer `sshkv` mit sudo als Standard,** `root` pro Host wählbar.
- **Ohne sshkv-Gruppe kein Zugang** – auch kein Lesezugriff.
- **Protokoll-Hash im Datenbank-Trigger:** Niemand kann eine Zeile ohne Verkettung einfügen; die ID wird erst unter Sperre vergeben, damit die Kette auch bei parallelem Schreiben hält.
- **Ein Bootstrap-Skript für Passwort- und Copy-Paste-Weg,** Dateizugriffe als Zielbenutzer (Symlink-Schutz).
- **Eigene Testdatenbank je parallelem Lauf:** Sub-Agent und Hauptsitzung leerten sich gegenseitig die Tabellen (in `CLAUDE.md` festgehalten).
- **Prisma-Client in den Worker gebündelt:** Im Standalone-Abbild fand die ESM-Auflösung `@prisma/client` sonst nicht (erst im echten Docker-Stapel aufgefallen).
- **Worker-Benutzer in Compose wählbar** (`SSHKV_WORKER_USER`), weil Docker-Secrets ohne Swarm die Rechte der Host-Datei tragen.
## 2026-10-12 · Sitzung 2 · 10:07–11:07
**Issue:** #012–#020
**Version:** 0.2.0
### Erledigt
- #012–#014 (Schlüssel, Personen/Self-Service, Private Key anzeigen) von einem Sub-Agenten in eigenem Worktree, danach gemergt; Konflikte in `useVorgang` und der Handler-Tabelle zusammengeführt
- #015 authorized_keys-Parser und Merge mit 26 Randfall-Tests
- #016 Public Keys ausrollen: Soll, Trockenlauf, Ausrollen, Hostseite, Schlüsselseite – Integrationstests und E2E
- #017 Private Key mit `~/.ssh/config` und `known_hosts` – Integrationstest „A meldet sich bei B an“, E2E für den ganzen Kern-Anwendungsfall
- Vorgezogen: #018 Zielkonto-Skripte (Integrationstests), #019 Vorlagen-Logik, #020 Verbindungstest
- Stand: 410 Unit-/DB-Tests, 30 Integrationstests, 12 E2E-Abläufe grün
### Entscheidungen
- **Phase 3 teilweise vorgezogen,** weil Florian „weiter“ sagte, während der Agent noch an Phase 2 arbeitete – nur Teile ohne Schemaänderung, damit die Migrationen des Agenten nicht kollidieren.
- **Trockenlauf und Ausrollen als zwei Vorgänge mit Hash-Sperre:** Es wird nie etwas anderes geschrieben, als jemand gesehen hat.
- **Private-Key-Ziele im Dry-Run nur für Admins:** Sonst hätte ein Operator einen vorbereiteten Private Key ausrollen lassen können (Freigabe Frage 3).
- **Keine Sicherungskopie alter Private Keys** auf dem Host; config-Block am Dateianfang, weil ssh den ersten Treffer nimmt.
- **Ein Hook `useVorgang` für alle:** zwei parallel entstandene Fassungen zusammengeführt, beide Schnittstellen bleiben.
## 2026-10-12 · Sitzung 3 · 11:06–11:36
**Issue:** #018–#020
**Version:** 0.3.0
### Erledigt
- #019 Vorlagen-Oberfläche (Liste, Formular, Live-Vorschau) von einem Sub-Agenten, gemergt
- #018 Zielkonten als Vorgänge mit Migration, Herkunft gemerkt; Integrationstest anlegen → entfernen
- #020 Host-zu-Host-Assistent mit Vorlagen, wiederaufnehmbarer Ausführung und Verbindungstest; E2E mit „Deployment-Zugang“
- Stand: 423 Unit-/DB-Tests, 31 Integrationstests, 16 E2E-Abläufe grün
### Entscheidungen
- **Migration auf Ortszeit-Reihenfolge umbenannt** (`20261012110000`): Prisma vergibt UTC-Zeitstempel, die Migrationen des Agenten trugen Ortszeit – sonst stünde die neue vor älteren.
- **Bestätigung im neuen Tab:** Der Assistent hält seine Eingaben im Speicher; eine Umleitung nach `/bestaetigen` hätte sie gelöscht.
- **Ausführung des Assistenten als Kette im Klick** statt als Hook-Zustandsmaschine – einfacher, und erledigte Schritte lassen sich beim Fortsetzen überspringen.
- **Prozesse stoppen nur über ein Skript, das Programm und Arbeitsverzeichnis prüft:** Zweimal hatte ein zu breites Suchmuster die eigene Shell getroffen.
## 2026-10-12 · Sitzung 4 · 12:50–13:35
**Issue:** #021–#025, #030
**Version:** 0.4.0
### Erledigt
- Zweites, getrenntes Paar SSH-Testziele (`sshkv-test-b`, Ports 2231/2232, Mock-OIDC 8091) für den Rotation/Widerruf-Agenten, damit Integrationstests sich nicht gegenseitig zurücksetzen
- #021 Abgleich mit reiner Befundlogik, Worker-Ablauf, Zeitplan; #022 Übernahme mit neuem Feld `ausserhalbEntfernen` – Integrationstests und E2E
- #023/#024 Rotation und Widerruf (Sub-Agent) mit Zwischenzustand `widerrufEingeleitetAm`; #025 Zuweisungsmatrix (Sub-Agent)
- #030 Gitea-Skript bereitgelegt (Trockenlauf läuft)
- Stand: 470 Unit-/DB-Tests, 38 Integrationstests, 21 E2E-Abläufe grün
### Entscheidungen
- **Abgleich ändert nichts auf dem Host:** Er meldet nur; Korrekturen laufen über Dry-Run und Ausrollen, damit ein Mensch sie sieht.
- **Bekannte Schlüssel außerhalb des Blocks sind kein Befund** – nur unbekannte. Sonst meldete jeder bewusst stehen gelassene Admin-Key dauerhaft Drift.
- **Widerruf mit Zwischenzustand:** Ein Schlüssel wird erst WIDERRUFEN, wenn er überall entfernt ist; ein nicht erreichbarer Host lässt ihn erkennbar „unvollständig“.
- **Rotation entfernt den alten Schlüssel erst nach erfolgreichen Verbindungstests**; doppelte Aliase in `~/.ssh/config`: die jüngste Zuweisung gewinnt.
- **Migrationszeitstempel in Ortszeit-Reihenfolge** auch bei parallel entstandenen Migrationen (`…130000` Agent, `…131000` Hauptsitzung).
## 2026-10-12 · Sitzung 5 · 16:27–17:12
**Issue:** #026–#029
**Version:** 0.5.0
### Erledigt
- #026 Übersicht aus der Datenbank; lesbare Aktionstexte aus #027
- #027 Audit-Log-Ansicht, Kettenprüfung, CSV, Syslog (Sub-Agent)
- #028 Benachrichtigungen und Einstellungsseite (Sub-Agent, eigener Mailpit und eigene Testziele)
- #029 Handbuch (12 Kapitel mit Bildern), Installation, Bedrohungsanalyse, Sicherung; Sicherung/Wiederherstellung geprobt
- Merge-Konflikte (Compose, Screenshot-Skript, Worker-Einstieg) zusammengeführt; der strukturelle Test für Aktionstexte fand vier neue Aktionen ohne Text
- Stand: 858 Unit-/DB-Tests, 43 Integrationstests, 24 E2E-Abläufe grün
### Entscheidungen
- **Wiederherstellung wirklich geprobt** statt nur beschrieben – sie fand zwei Fehler, die im Ernstfall die Wiederherstellung verhindert hätten.
- **Master-Key nicht in die Sicherung,** nur ein Prüfwert; Worker verweigert den Start mit falschem Master-Key.
- **Benachrichtigungen lesen ihre Anlässe aus der Datenbank** statt sich in den Abgleich einzuhängen; Doppelmeldungen verhindert ein Versandprotokoll mit Fingerabdruck je Anlass.
- **Keine 1.0.0 ohne Abnahme:** 0.5.0 schließt die fünf Phasen; authentik, Gitea und ein Lauf auf echten Hosts stehen noch aus.
## 2026-10-12 · Sitzung 6 · 17:12–18:07
**Issue:** #030–#032
**Version:** unveröffentlicht (auf dem Weg zu 1.0.0)
### Erledigt
- Remote-Repo in Gitea: `main` und Tags v0.1.0–v0.5.0 gepusht, Issues abgeglichen (Basic-Auth, weil die hinterlegten Zugangsdaten ein Passwort sind)
- #031 Geführte Rotation des Deployment-Keys: Service, Worker-Schritte, Seite mit Schrittleiste, Integrationstest gegen beide Testziele, E2E
- #032 Nacharbeiten (Sub-Agent): lückenlose Protokoll-IDs, Rotation abbrechen, Anker „Zum Dry-Run“, Massenwiderruf-E2E – der fand, dass der Massenwiderruf UUIDs mit Bindestrich abwies
- Stand: 876 Unit-/DB-Tests, 46 Integrationstests, 27 E2E-Abläufe grün
### Entscheidungen
- **Der Nachfolger des Deployment-Keys ist ein eigener Schlüssel,** der während der Rotation über `sollFuer()` in Block und Pflichtliste steht – so verteilt ihn der normale Dry-Run, und der Aussperrschutz deckt beide Keys.
- **Umstellen erst nach Prüfung** auf allen Hosts; ein Host lässt sich nur bewusst und protokolliert übergehen.
- **Rotation abbrechen nutzt den vorhandenen Widerruf** statt eines neuen Zustands (#032).
- **Protokoll-ID nur noch aus dem Trigger** unter der Advisory-Sperre – keine Lücken mehr durch verworfene Sequenzwerte.
## 2026-10-12 · Sitzung 7 · 19:48–20:18
**Issue:** #033, #034
**Version:** unveröffentlicht (auf dem Weg zu 1.0.0)
### Erledigt
- #033 Nach einer abgebrochenen Rotation lässt sich derselbe Schlüssel erneut rotieren (1:n-Relation, partieller Index)
- #034 Abnahme-Checkliste `docs/betrieb/abnahme.md` für authentik, echte Hosts und Betrieb
- Stand: 877 Unit-/DB-Tests, 46 Integrationstests grün; E2E Rotation, Widerruf, Schlüssel grün
### Entscheidungen
- **Je Vorgänger höchstens ein offener Nachfolger** statt genau einem: abgebrochene bleiben in der Historie, maßgeblich ist der jüngste.
- **Neustart erst nach abgeschlossenem Abbruch** – sonst stünden zwei neue Schlüssel gleichzeitig auf den Hosts.
- **Bestätigung „Rotation abgebrochen“ bleibt sichtbar** über dem Neustart, statt kommentarlos auf Schritt 1 zu springen (fand der E2E-Test).
## 2026-10-12 · Sitzung 8 · 22:05–22:55
**Issue:** #035, #036
**Version:** unveröffentlicht (auf dem Weg zu 1.0.0)
### Erledigt
- #035 Lokale Benutzerkonten mit Pflicht-TOTP: Einmal-Passwort, eigenes Passwort bei der ersten Anmeldung, Benutzerverwaltung unter Einstellungen, Schalter `SSHKV_LOKALE_KONTEN`
- #036 Rotation des Deployment-Keys vor dem Umstellen abbrechen
- Stand: 911 Unit-/DB-Tests, 47 Integrationstests, 29 E2E-Abläufe grün
### Entscheidungen
- **TOTP auch für lokale Konten Pflicht; lokale Konten dauerhaft, aber abschaltbar** (Florian).
- **Admins wählen keine Passwörter für andere** – sshkv erzeugt ein Einmal-Passwort, das genau einmal angezeigt wird.
- **`SSHKV_LOKALE_KONTEN` muss in `docker-compose.yml` stehen:** Compose reicht nur ausdrücklich genannte Variablen an den Web-Container weiter – sonst hätte der Schalter im Betrieb nicht gewirkt.
- **Testreste machen Produktlücken sichtbar:** Eine hängengebliebene Rotation auf verwaisten Testhosts zeigte, dass es keinen Weg zurück gab (#036). Die E2E-Tests räumen solche Reste jetzt selbst ab.