Eine begonnene Rotation ließ sich nicht zurücknehmen; nahm ein Host den
neuen Key dauerhaft nicht an, blieb nur das Übergehen. Abbrechen
verwirft jetzt den neuen Key samt Private Key, ein Dry-Run nimmt ihn
wieder von den Hosts, und eine neue Rotation kann sofort starten.
Nebenbei: Aktionen der Benutzerverwaltung (#035) bekommen im Audit-Log
eine eigene Gruppe, statt den Filter „Anmeldung“ zu verwässern; der
E2E-Test für lokale Konten hängt nicht mehr von der Reihenfolge ab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ohne authentik gab es nur den Notfall-Admin – ein geteiltes Konto ohne
Rollen und ohne erkennbare Akteure im Audit-Log. Admins legen jetzt
lokale Konten an; sshkv erzeugt ein Einmal-Passwort, das bei der ersten
Anmeldung ersetzt wird, danach ist TOTP Pflicht.
Rollen, Sperren (beendet Sitzungen sofort) und Zurücksetzen unter
Einstellungen → Benutzer, nur für Admins mit frischer Bestätigung.
SSHKV_LOKALE_KONTEN=0 schaltet lokale Konten ab; der Notfall-Admin
bleibt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die automatischen Tests laufen gegen Mock-OIDC und Testcontainer.
Echter authentik, echte Hosts und der Betrieb brauchen eine eigene,
abhakbare Prüfung mit erwartetem Ergebnis je Punkt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bisher war vorgaengerId eindeutig: nach einer abgebrochenen Rotation
passte kein zweiter Nachfolger in die Kette, und der Schlüssel ließ sich
nur über Neuanlage, Zuweisen und Widerruf ersetzen.
Jetzt ist die Relation 1:n. Ein partieller Index erlaubt je Vorgänger
nur einen Nachfolger, der weder widerrufen ist noch gerade widerrufen
wird. Maßgeblich ist der jüngste. Erst wenn der Abbruch abgeschlossen
ist, beginnt die Rotation wieder bei Schritt 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Haelt fest, was die Nacharbeiten gefunden und entschieden haben:
den Fehler im Massenwiderruf (UUIDs fielen aus der Auswahl), das
Zustandsmodell des Rotationsabbruchs ohne neues Feld, warum es danach
keinen zweiten Nachfolger gibt, und wie die Protokoll-IDs ohne
Umnummerieren lueckenlos werden.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bisher ließ sich der Instanz-Key nur von Hand auf jedem Host tauschen.
Die Rotation läuft jetzt in fünf Schritten: Nachfolger erzeugen, per
Dry-Run neben den alten verteilen, Anmeldung je Host prüfen, umstellen,
alten Key aus allen Blöcken entfernen und seinen Private Key verwerfen.
Der Nachfolger steht während der Rotation in Block und Pflichtliste,
damit kein Host ohne funktionierenden Key dasteht. Umgestellt wird nur
nach erfolgreicher Prüfung oder bewusstem Übergehen eines Hosts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In den Git-Zugangsdaten steht das Konto-Passwort, kein API-Token. Gitea
nimmt es nur per Basic-Auth an; das Skript erkennt das jetzt selbst.
Alle 30 Issues mit Labels und Meilensteinen uebertragen. Neu: #031
Rotation des Deployment-Keys und #032 Nacharbeiten fuer 1.0.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der E2E-Lauf belegt im gebauten Stand, was die Unit- und DB-Tests nur
einzeln zeigen: ein Admin speichert SMTP, die Testnachricht kommt ueber
den Worker in Mailpit an, das Passwort kehrt nie in die Seite zurueck,
und ein Leser sieht keine Aendern-Knoepfe. Die Screenshots (1440/390 px)
halten das Layout der neuen Seite fuer die Anleitung fest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Das Protokoll war bisher nur in der Datenbank lesbar. Die Ansicht zeigt es
jeder Rolle mit lesbaren Aktionstexten, aufklappbarem Diff, Details und
Hashes; ein Test haelt fest, dass jede Aktion im Code einen Text hat.
Geblaettert wird ueber die ID statt OFFSET, weil das Protokoll waehrend
des Lesens waechst. Die Kettenpruefung liest jede Zeile und laeuft deshalb
nur auf Knopfdruck, mit 5 Minuten Zwischenspeicher; ein Bruch wird selbst
protokolliert. Der CSV-Export streamt in Stapeln, entschaerft Formeln und
steht selbst im Protokoll. protokollieren() reicht jeden Eintrag nach dem
Schreiben an Syslog weiter, ohne Details und Diff.
Kein Weg aendert oder loescht Eintraege; ein struktureller Test sichert
das ab, zusaetzlich zum Trigger.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kennzahlen, Hosts mit Abweichung, bald ablaufende Schluessel und letzte
Aktionen kommen aus der Datenbank; die Beispieldaten des Prototyps sind
entfernt. Befunde werden je Host und Benutzer in fester Reihenfolge
zusammengefasst, abgelaufene und noch ausgerollte Schluessel stehen in
Koralle. Anmeldungen bleiben dem Audit-Log vorbehalten, sonst
verdraengten sie auf der Uebersicht alles andere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Einen Schlüssel zu tauschen soll keinen Ausfall riskieren: Der Nachfolger
übernimmt alle aktiven Zuweisungen, wird ausgerollt und per
Verbindungstest geprüft – erst wenn alle Tests seit dem letzten Ausrollen
grün sind, entfernt der Widerruf aus #024 den alten Schlüssel, der dann
als ROTIERT endet.
Private Keys bekommen einen neuen Pfad (-rJJJJMMTT) bei gleichem Alias.
Damit ssh den neuen nimmt, schreibt privat.ts je Alias nur noch einen
config-Eintrag; die jüngste Zuweisung gewinnt. Der Worker setzt
vorgaengerId schon beim Erzeugen, so ist die Kette auch nach einem
Abbruch vollständig und die Schrittfolge wiederaufnehmbar.
Personen-Keys ohne Private Key in sshkv bekommen nur einen Hinweis:
dafür braucht es einen neuen Public Key von der Person.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ein Schlüssel soll nach Projektende mit einem Klick von allen Hosts
verschwinden – aber nie als „widerrufen“ gelten, solange er irgendwo noch
liegt. Deshalb ein Zwischenzustand (widerrufEingeleitetAm): Verwendungen
werden zum Entfernen vorgemerkt, der bestehende Dry-Run mit Hash-Sperre
zeigt und schreibt die Änderungen, und erst wenn keine Verwendung mehr
offen ist, setzt der Abschluss WIDERRUFEN (bzw. ROTIERT für #023).
Scheitert ein Host, bleibt der Schlüssel im Zwischenzustand und die Seite
nennt die offenen Ziele.
Zielkonten lassen sich optional mit entfernen, aber erst nach den
Schlüsseln und nie, wenn ein anderer aktiver Schlüssel sie noch nutzt.
Massenwiderruf (Admin, frische Bestätigung) nutzt einen gemeinsamen
Dry-Run und protokolliert je Schlüssel. Abgelaufene Schlüssel erscheinen
im Filter „abgelaufen“ und bieten den Widerruf an – entfernt wird nie
automatisch.
Migration 20261012130000_widerruf_eingeleitet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bisher liess sich nur je Schluessel oder je Host nachsehen, wo ein Key
verwaltet ist. Die Matrix zeigt das auf einen Blick, loest
Hostgruppen-Zuweisungen je Mitglied auf (P vs. P·G) und macht nicht
synchrone Zustaende ohne Farbe erkennbar.
Die Matrix plant nur: Zuweisen und Entfernen laufen ueber die
vorhandenen Aktionen, ausgerollt wird weiter per Dry-Run auf der
Schluesselseite. Der Aufbau ist eine reine Funktion in src/lib, damit
Seite und CSV-Export garantiert dasselbe zeigen und die
Gruppenaufloesung ohne Datenbank testbar ist. Der CSV-Export entschaerft
Formelbeginne, weil Schluesselnamen frei waehlbar sind und Excel sie
sonst ausfuehren wuerde.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ein Abgleich liest je Host alle authorized_keys in einem SSH-Aufruf,
als jeweiliger Benutzer, und vergleicht sie mit dem Soll: fehlende,
von Hand geaenderte und unbekannte Keys, falsche Rechte, beschaedigte
Bloecke und veraenderte Private Keys. Befunde werden geoeffnet und
wieder geschlossen, Zustaende von Verwendungen und Host nachgezogen.
Auf dem Host aendert der Abgleich nichts.
Der Zeitplan liegt in pg-boss und feuert auch bei mehreren Workern
nur einmal. Auf der Hostseite stehen Abweichungen und der Ist-Zustand
je Benutzer; unbekannte Keys lassen sich uebernehmen - nur bekannt
machen oder mit Dry-Run in den verwalteten Block holen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Vorlagen-Logik aus #019 war bisher nur per Seed und Service
erreichbar. Admins koennen eigene Vorlagen jetzt anlegen, eingebaute
anpassen und eigene archivieren; jede Rolle sieht die Liste, weil
Operatoren die Vorlagen im Assistenten wiedererkennen sollen.
Die Vorschau nutzt im Browser dieselben reinen Funktionen wie der
Server (platzhalterErsetzen, optionenPruefen, sudoersInhalt). So sieht
der Admin vor dem Speichern, was in authorized_keys, ~/.ssh/config und
sudoers landen wuerde; verbindlich prueft weiterhin der Server. Voller
sudo-Zugang bekommt einen deutlichen Hinweis, weil er root gleichkommt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schluessel, Personen, Private Key anzeigen, authorized_keys-Parser und
Ausrollen von Public Keys; vorgezogen das Ausrollen von Private Keys
mit ssh-config und known_hosts. CHANGELOG und DEVLOG nachgetragen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Admins koennen einen gespeicherten Private Key nach frischer
Bestaetigung einmalig anzeigen oder als Datei <slug> herunterladen und
ihn endgueltig aus SSHKeyVault loeschen.
Warum so:
- Der Web-Prozess hat keinen Master-Key. Er bittet den Worker per
Vorgang, den Key zu oeffnen; der legt ihn mit SSHKV_APP_SECRET
verschluesselt in Vorgang.ergebnis ab. Das Web holt ihn genau einmal
ab: ein einziges UPDATE unter Zeilensperre liest und ueberschreibt.
- Nicht abgeholte Keys verfallen nach zwei Minuten: Worker-Takt alle
30 s, Aufraeumen vor jeder Abholung, und das Web gibt aeltere nicht
mehr heraus. Die Status-Schnittstelle zeigt nur "bereit".
- Download nur per POST (ein GET-Link oder Vorabruf wuerde die
Abholung verbrauchen), no-store, Herkunft per Sec-Fetch-Site, weil
no-referrer bei Formular-POSTs Origin: null sendet.
- Loeschen laeuft als Worker-Vorgang, damit nur geheimnisse.ts die
Chiffrat-Felder anfasst; eigenes Recht privateKeyLoeschen mit
Bestaetigung. Deployment-Keys gibt sshkv weder heraus noch loescht es
sie.
- Protokoll: angefordert, angezeigt, heruntergeladen, geloescht - mit
Fingerprint, nie mit Key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Personen lassen sich anlegen, bearbeiten und archivieren; ihr Detail
zeigt ihre Schluessel. Unter "Meine Schluessel" hinterlegt jede Rolle
eigene Public Keys (Freigabe Frage 4), wirksam erst nach Zuweisung
durch einen Operator oder Admin.
Warum so:
- Die eigene Person entsteht beim ersten Aufruf oder wird verknuepft:
per benutzerId, sonst per E-Mail - aber nur, wenn der OIDC-Anbieter
die Adresse bestaetigt hat. Dafuer merkt sich Benutzer jetzt
emailBestaetigt (Migration); sonst koennte jemand mit selbst
gewaehlter Adresse die Keys einer fremden Person sehen.
- Die Self-Service-Services nehmen nur den angemeldeten Benutzer und
leiten die Person selbst ab; fremde Schluessel-IDs gelten als nicht
vorhanden.
- Entfernen nur fuer eigene, selbst hinterlegte Keys ohne Verwendung,
bedingt geloescht, damit eine gleichzeitige Zuweisung nicht per
Cascade verschwindet.
- Archivieren statt Loeschen: wer bis wann welchen Key hatte, bleibt
nachvollziehbar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schluessel werden jetzt verwaltet statt nur im Prototyp gezeigt: Liste
mit Filter, Anlegen (erzeugen oder importieren), Detail nach dem
Prototyp aus #002, Bearbeiten der Metadaten und Download <slug>.pub.
Warum so:
- Erzeugen und Import eines Paars laufen als Vorgang im Worker, weil
nur er den Master-Key hat. Passphrase und importierter Private Key
reisen wie das Bootstrap-Passwort mit SSHKV_APP_SECRET verschluesselt
im Job, nie in Vorgang.eingabe; der Worker loescht den Job danach.
- Ein reiner Public Key ist kein Geheimnis und wird direkt im Web
angelegt, mit verstaendlicher Meldung bei einem Duplikat.
- Das Web liest nie das Chiffrat: ob ein Private Key gespeichert ist,
verraet masterKeyVersion (per CHECK an das Chiffrat gekoppelt). Ein
struktureller Test verlangt fuer jede zeilenliefernde Abfrage auf
Schluessel ausserhalb des Workers ein festes select.
- Migration: Vorgangsarten fuer Erzeugen, Import und (fuer #014) das
Loeschen des Private Keys; CHECKs fuer den Slug (landet in
Dateinamen) und "geloescht nur ohne Chiffrat".
- Platzhalter PublicAusrollenAktion/PrivateAusrollenAktion fuer #016/#017.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reine Funktionen fuer Dry-Run und Ausrollen. Alles ausserhalb des
Blocks bleibt bytegenau stehen, auch CRLF, BOM und kaputte Zeilen;
der Block entsteht deterministisch aus dem Soll. Ein beschaedigter
Block wird nie automatisch ueberschrieben, weil genau dort fremde
Zeilen verschwinden koennten.
Verwaltete Keys erkennt sshkv am Fingerprint, nicht am Kommentar. Der
Deployment-Key kann nicht aus dem Block fallen (Aussperrschutz).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auf der Hostseite: Host-Key abrufen, Fingerprint vergleichen und
vertrauen oder ablehnen. Weicht ein Key vom vertrauten ab, erscheint
eine deutliche Warnung, und Annehmen geht nur mit Haekchen und frischer
Bestaetigung. Danach Bootstrap per Einmal-Passwort oder per Befehl und
Verbindungspruefung, jeweils mit Fortschrittsanzeige.
Der sshd-Befund empfiehlt prohibit-password, wenn root sich per
Passwort anmelden darf - nur als Hinweis, geaendert wird nichts.
E2E im gebauten Stand mit Worker gegen ein echtes sshd.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SSHKV_SEED_DEMO=1 legt sechs Demo-Hosts, drei Gruppen und einen Kunden an,
damit Entwicklung, Screenshots und Handbuch ohne echte Server auskommen.
Nur mit der Variable, damit eine echte Instanz nie Phantom-Hosts bekommt;
Upsert nach Name haelt den Seed idempotent.
Das Screenshot-Skript kennt jetzt Seiten mit wechselnder ID (folgen: erster
passender Link) und nimmt Hostliste und Hostdetail auf.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der zweite Teil von #006 bindet den Kern an Datenbank und Worker. Nur
src/worker/geheimnisse.ts liest Chiffrat und Data-Key; ein struktureller
Test haelt fest, dass der Webprozess nichts von dort importiert und so
nie an den Master-Key kommt.
Die Zusatzdaten binden jedes Chiffrat an ID und Fingerprint seines
Schluessels: kopiert man es in eine andere Zeile, laesst es sich dort
nicht oeffnen. Der Deployment-Key entsteht beim ersten Start; ein Test
prueft, dass ein pg_dump keinen Klartext enthaelt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Details und Diff laufen durch schwaerzen(), damit kein Private Key und
kein Passwort ins unveraenderliche Protokoll geraet - was dort einmal
steht, laesst sich per Trigger nicht mehr entfernen.
Der Wortstamm privat ist dafuer zu breit: er schwaerzte auch Pfade und
Verwendungszaehler, die das Protokoll gerade zeigen soll. Jetzt greifen
nur Felder, die den Schluessel selbst tragen, und jeder PEM-Block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Anmeldung, Einrichtung des zweiten Faktors fuer den Notfall-Admin,
erneute Bestaetigung vor kritischen Aktionen und die Seite fuer
fehlende Rechte. Die Huelle zeigt jetzt den echten Benutzer, und jede
Seite prueft die Sitzung selbst; ein struktureller Test haelt das fest.
Die Ablaeufe sind im gebauten Stand gegen den Mock-OIDC geprueft
(pnpm test:e2e): Admin, Konto ohne Gruppe, Notfall-Admin mit TOTP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Oberflaeche legt einen Vorgang an und schickt nur dessen ID in die
Warteschlange; der Worker fuehrt ihn aus und haelt Status, Ergebnis
oder Fehler fest. So bleiben lange SSH-Vorgaenge aus dem Webprozess
heraus, und der Fortschritt ist ohne Job-Interna sichtbar.
Kein automatisches Wiederholen: ein halb ausgefuehrter Schreibvorgang
auf einem Host gehoert vor einen Menschen. Fehlertexte aus fremden
Bibliotheken erreichen die Oberflaeche nie.
Dockerfile und Compose fuer den Betrieb: Master-Key als Secret nur im
Worker, Datenbank in einem internen Netz.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/screenshots.ts nimmt Uebersicht und Schluesseldetail bei
1440x900 und 390x844 auf und meldet nebenbei horizontalen Ueberlauf,
damit es fuer die Anleitung (#029) und als schneller Layout-Test taugt.
Issue #002 ist damit erledigt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der framework-freie Kern ist fertig; Deployment-Key, Master-Key im Worker
und der Nachweis am Datenbank-Dump haengen an Datenbank und Worker und
kommen mit #007. Die Entscheidungen (16 bcrypt-Runden, Fehlercodes,
RSA-Mindestlaenge abschaltbar fuers Beobachten, Umschlag-Format,
Dateirechte des Master-Keys) stehen im Issue, damit die Anbindung nicht
raten muss.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bootstrap, Host-Key-Pruefung und spaeteres Ausrollen brauchen echte
sshd-Gegenstellen. Zwei Debian-13-Container (ziel-a, ziel-b) in einem
gemeinsamen Netz bilden frische Hosts vor dem Bootstrap nach: root- und
Passwort-Login sind absichtlich offen, Host-Keys entstehen erst beim
Start, und `hostkeys-tauschen` erlaubt den Test "geaenderter Host-Key
wird abgelehnt" ohne Neuerstellen des Containers.
Die Testhilfen lesen dieselben Adress-Variablen wie die Compose-Datei
und lassen Tests mit Hinweis auf den Startbefehl fehlschlagen, statt
sie still zu ueberspringen - ein uebersprungener Integrationstest sieht
sonst wie ein bestandener aus.
Der Mock-OIDC-Anbieter ersetzt authentik in der Entwicklung (PLAN.md,
Entscheidung 7). Bei interaktivem Login uebernimmt er nur Claims aus dem
Formular, daher belegt eine eigene Login-Seite email, name und groups
vor und laesst die Gruppen frei waehlen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alle Modelle fuer die fuenf Phasen in einer Migration, damit spaetere
Phasen keine Umbauten an Kerntabellen brauchen. Was auch an der
Service-Schicht vorbei gelten muss, steht als CHECK, partieller Index
oder Trigger in der Datenbank: Personen-Keys ohne Private Key, genau
ein Instanz-Deployment-Key, ein vertrauter Host-Key je Algorithmus,
shell-sichere Zielbenutzer und Pfade.
Das Protokoll ist append-only und per Hash-Kette verbunden; den Hash
berechnet ein Trigger, damit keine Zeile ohne Verkettung entstehen
kann (#005 baut darauf auf).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Konfiguration aus Laufzettel uebernommen, damit alle Projekte gleich
gebaut und geprueft werden. Entwicklungsdatenbank auf 5436, weil 5433
und 5434 von ProxLedger und Laufzettel belegt sind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plan nach Freigabe vom 12.10.2026 mit den Antworten auf die sieben
Rueckfragen. Issues fuer alle fuenf Phasen als Dateien, bis der
Gitea-Zugang da ist (#030).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>