Zeiten und Entscheidungen der Sitzung festhalten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
205 lines
13 KiB
Markdown
205 lines
13 KiB
Markdown
# 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.
|
||
|