docs(betrieb): Handbuch, Bedrohungsanalyse, geprobte Sicherung (#029)

Handbuch mit Bildern fuer alle Ablaeufe, darunter der Kern-
Anwendungsfall und der Deployment-Zugang fuer Claude Code.
Betriebsdoku zu Installation, authentik und Reverse Proxy sowie eine
Bedrohungsanalyse mit Notfallplan fuer den Fall, dass sshkv selbst
kompromittiert wird.

Sicherung und Wiederherstellung als Skripte, auf einem Wegwerf-Stapel
geprobt. Dabei fielen zwei Fehler auf: das Schema der Warteschlange
liess sich nicht zurueckspielen, und ein Abbruch ohne Transaktion
loeschte es halb. Jetzt nur Schema public, in einer Transaktion, mit
Pruefwert des Master-Keys. Der Worker startet nicht, wenn der
Master-Key nicht zur Datenbank passt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 16:46:38 +02:00
co-authored by Claude Opus 5.5
parent f791816e85
commit 3b28f73c95
13 changed files with 682 additions and 3 deletions
+112
View File
@@ -0,0 +1,112 @@
# Installation und Betrieb
SSHKeyVault läuft als Docker-Compose-Stapel aus drei Diensten plus einem einmaligen Migrationslauf:
| Dienst | Aufgabe | Netz | Geheimnisse |
|---|---|---|---|
| `db` | PostgreSQL 17 | nur intern (`internal: true`) | Datenbankpasswort |
| `migrate` | Schema einspielen, Notfall-Admin und Vorlagen anlegen (läuft bei jedem Start einmal) | intern | Notfall-Passwort beim ersten Start |
| `web` | Weboberfläche (Port 3400) | intern + nach außen zum OIDC-Anbieter | `SSHKV_APP_SECRET`, OIDC-Client-Secret – **kein Master-Key** |
| `worker` | SSH zu den Hosts, Ver-/Entschlüsselung, Zeitpläne | intern + zu den Hosts | Master-Key (Docker-Secret), `SSHKV_APP_SECRET` |
## Voraussetzungen
- Linux-Host mit Docker und Docker Compose v2, **im internen Netz bzw. nur per VPN erreichbar**.
- Der Worker muss die zu verwaltenden Hosts per SSH erreichen (Port 22 oder den jeweiligen Port).
- authentik (oder ein anderer OIDC-Anbieter) für die Anmeldung.
- Ein Reverse Proxy mit TLS vor der Weboberfläche.
## Schritt für Schritt
```bash
sudo mkdir -p /opt/sshkv && cd /opt/sshkv
git clone <repo> . # oder Release entpacken
cp .env.example .env && chmod 600 .env
```
In `.env` eintragen (Kommentare in der Datei erklären jede Variable):
- `POSTGRES_PASSWORD` – `openssl rand -hex 24`
- `SSHKV_APP_SECRET` – `openssl rand -base64 48`
- `SSHKV_APP_URL` – die Adresse, unter der Benutzer sshkv aufrufen, z. B. `https://sshkv.intern.example.de`
- `SSHKV_BREAKGLASS_PASSWORD` – mindestens 16 Zeichen; Notfall-Admin `notfall`
- `SSHKV_OIDC_*` – siehe unten
Master-Key erzeugen und nur für den Worker lesbar machen:
```bash
sudo mkdir -p geheim
sudo sh -c 'head -c 32 /dev/urandom > geheim/master.key'
sudo chown 1001:1001 geheim/master.key && sudo chmod 400 geheim/master.key
```
Starten:
```bash
docker compose up -d --build
docker compose logs -f worker # „sshkv-worker … bereit“
```
Beim ersten Start legt `migrate` das Schema, den Notfall-Admin und die eingebauten Vorlagen an; der
Worker erzeugt den **Deployment-Key** der Instanz. Danach sofort den Master-Key sichern
([sicherung.md](sicherung.md)).
## authentik einrichten
1. **Provider** „OAuth2/OpenID Provider“ anlegen: Client-Typ *confidential*, Redirect-URI
`https://sshkv.intern.example.de/api/auth/oidc/rueckruf`, Scopes `openid profile email` und ein
Scope-Mapping, das die Gruppen als Claim `groups` liefert (in authentik Standard).
2. **Application** „SSHKeyVault“ mit diesem Provider anlegen.
3. **Gruppen** `sshkv-admins`, `sshkv-operators`, `sshkv-readers` anlegen und Benutzer zuordnen
(Namen änderbar über `SSHKV_OIDC_GROUP_*`). Wer in keiner Gruppe ist, kommt nicht hinein.
4. In `.env`: `SSHKV_OIDC_ISSUER=https://authentik.example.de/application/o/sshkv/`,
`SSHKV_OIDC_CLIENT_ID`, `SSHKV_OIDC_CLIENT_SECRET`, `SSHKV_OIDC_NAME=authentik`.
Erneute Bestätigung vor kritischen Aktionen schickt `prompt=login` und `max_age=0`; sshkv prüft
`auth_time` im ID-Token. authentik liefert `auth_time` standardmäßig.
| Rolle | darf |
|---|---|
| Leser | alles ansehen, eigene Public Keys hinterlegen |
| Operator | zusätzlich Hosts verwalten, Host-Keys bestätigen, Schlüssel erzeugen, Public Keys ausrollen, abgleichen |
| Admin | zusätzlich Private Keys ausrollen/anzeigen/löschen, Zielkonten mit sudo, Massenwiderruf, Einstellungen, Vorlagen |
## Reverse Proxy (Beispiel Caddy)
```caddyfile
sshkv.intern.example.de {
tls internal # oder das interne Zertifikat
@extern not remote_ip 10.0.0.0/8 192.168.0.0/16
respond @extern 403
reverse_proxy 127.0.0.1:3400
}
```
Wichtig: `SSHKV_APP_URL` muss die Adresse mit `https://` sein – davon hängen Secure-Cookie und
OIDC-Rückruf ab.
## Hosts vorbereiten
- **Debian/Ubuntu** mit `openssh-server`, `sudo`, `runuser` (util-linux), `base64`, `sha256sum`,
`visudo` (bei Dienstbenutzer) – auf Standardinstallationen vorhanden.
- Für den Bootstrap per Passwort einmalig ein Konto mit Passwort-Login (root oder ein Benutzer mit
sudo). Danach kann Passwort-Login aus; sshkv zeigt den sshd-Befund an.
## Aktualisieren
```bash
cd /opt/sshkv
sudo bash deploy/sichern.sh
git pull
docker compose up -d --build # migrate spielt neue Migrationen ein
```
## Variablen (Auswahl)
| Variable | Bedeutung |
|---|---|
| `SSHKV_SYNC_CRON` | Zeitplan des Abgleichs (Standard `30 2 * * *`, Europe/Berlin) |
| `SSHKV_MELDUNG_CRON` | tägliche Zusammenfassung der Benachrichtigungen |
| `SSHKV_SYSLOG_HOST`/`_PORT`/`_PROTOKOLL` | Kopie des Protokolls an einen Syslog-Server |
| `SSHKV_WORKER_USER` | Benutzer des Worker-Containers, falls die Master-Key-Datei nicht UID 1001 gehört |
| `SSHKV_BIND` / `SSHKV_PORT` | Bindeadresse/Port der Weboberfläche (Standard `127.0.0.1:3400`) |
+84
View File
@@ -0,0 +1,84 @@
# Sicherheit und Bedrohungsanalyse
SSHKeyVault (`sshkv`) verwaltet die SSH-Zugänge zu Servern – und hat dafür selbst
**root-gleichen Zugriff auf alle verwalteten Hosts**. Wer sshkv übernimmt, übernimmt die
Infrastruktur. Diese Seite beschreibt, wie sshkv aufgebaut ist, um das zu erschweren, was
trotzdem passieren kann und was im Ernstfall zu tun ist.
## Was sshkv besitzt
| Wert | Wo | Schutz |
|---|---|---|
| **Deployment-Key** (Private Key) | Datenbank, verschlüsselt | Envelope Encryption mit Master-Key; nur der Worker entschlüsselt |
| **Private Keys** von Service-Schlüsseln | Datenbank, verschlüsselt | wie oben; Anzeige nur für Admins mit frischer Bestätigung, protokolliert |
| **Master-Key** | Datei auf dem Docker-Host, nur im Worker eingebunden | Dateirechte 0400, nicht in der Datenbank, nicht im Image, nicht im Repo |
| `SSHKV_APP_SECRET` | `.env` | signiert Sitzungen, verschlüsselt TOTP-Geheimnis, SMTP-Passwort, Webhook-Geheimnis und Job-Transporte |
| Host-Keys der Hosts | Datenbank | öffentlich; schützen vor Man-in-the-Middle (strikte Prüfung) |
| Protokoll | Datenbank | append-only per Trigger, Hash-Kette, optional Syslog |
**Personen-Keys** speichert sshkv nur als Public Key – ein Datenbankregel verhindert, dass dort je ein
Private Key steht. Bootstrap-Passwörter werden nie gespeichert.
## Aufbau, der Angriffe erschwert
- **Web und Worker getrennt.** Der Webprozess kennt den Master-Key nicht und spricht kein SSH. Eine
Lücke in der Weboberfläche allein entschlüsselt keine Private Keys; ein Test hält strukturell fest,
dass der Webprozess nichts aus `src/worker` importiert und nur eine einzige Datei die
verschlüsselten Felder liest.
- **Envelope Encryption** (AES-256-GCM): je Private Key ein eigener Datenschlüssel, gebunden an ID und
Fingerprint des Schlüssels. Ein Datenbank-Dump allein gibt keinen Schlüssel preis (Test).
- **Strikte Host-Key-Prüfung** bei jeder Verbindung. Ein geänderter Host-Key bricht die Verbindung vor
dem Senden von Zugangsdaten ab; Ersetzen nur mit Häkchen und frischer Bestätigung.
- **Dateien auf den Hosts** schreibt sshkv als jeweiliger Zielbenutzer (`runuser`): Ein Symlink im
Home eines Benutzers kann root nicht dazu bringen, `/etc/shadow` zu lesen oder zu überschreiben
(Integrationstest).
- **Dry-Run mit optimistischer Sperre:** Ausgerollt wird nur, was ein Mensch gesehen hat. Hat sich die
Datei oder die Zuweisung seitdem geändert, bricht der Vorgang ab.
- **Rollen:** Private Keys anzeigen, ausrollen und löschen, Deployment-Key, Zielkonten mit sudo und
Massenwiderruf nur für Admins; kritische Aktionen zusätzlich mit erneuter Anmeldung (5 Minuten).
Ohne passende authentik-Gruppe gibt es keinen Zugang, auch keinen lesenden.
- **Notfall-Admin** nur mit Passwort **und** TOTP; Rate-Limit gegen Raten.
- **Weboberfläche:** Content-Security-Policy mit Nonce, `X-Frame-Options: DENY`, `Referrer-Policy:
no-referrer`, HttpOnly-/SameSite-Cookies, Secure hinter HTTPS; Server Actions mit Origin-Prüfung.
## Was passiert, wenn sshkv kompromittiert wird?
| Angreifer hat … | Folge | Gegenmaßnahme |
|---|---|---|
| nur ein **Datenbank-Backup** | Hosts, Zuweisungen, Public Keys, Protokoll sichtbar; **keine** Private Keys | Backup trotzdem vertraulich behandeln (verrät die Struktur der Infrastruktur) |
| **Datenbank + Master-Key** | alle Private Keys inkl. Deployment-Key → root auf allen Hosts | Master-Key getrennt aufbewahren; Notfallplan unten |
| die **Weboberfläche** als Admin (gestohlene Sitzung) | kann Schlüssel zuweisen und ausrollen; Private Keys nur nach erneuter Anmeldung | kurze Sitzungen (8 h), Re-Auth, Protokoll + Syslog zeigen jede Aktion |
| den **Webprozess** (Codeausführung) | Datenbankzugriff, aber kein Master-Key | Worker getrennt; Master-Key nur im Worker-Container |
| den **Worker** oder Docker-Host | alles | Host härten, nur internes Netz, Updates, Zugriff auf den Docker-Host streng begrenzen |
| einen **verwalteten Host** | kann sshkv nicht angreifen; sieht nur den Public Key des Deployment-Keys | — |
### Notfallplan „sshkv kompromittiert“
1. **Sofort:** Netzwerkzugang zu sshkv sperren (Firewall/VPN), Container anhalten
(`docker compose stop`). Die Hosts bleiben erreichbar – sshkv ist kein Teil des Logins.
2. **Deployment-Key auf allen Hosts entfernen:** Auf jedem Host in `~sshkv/.ssh/authorized_keys`
(bzw. `/root/.ssh/authorized_keys` bei Anmeldeart root) die Zeile mit `sshkv-deployment@…` löschen
und `/etc/sudoers.d/sshkv` entfernen. Mit einem Konfigurationsmanagement oder einer Schleife über
die Hostliste (CSV-Export der Zuweisungen bzw. Hostliste).
3. **Ausgerollte Service-Keys tauschen:** Jeder Private Key, den sshkv kannte, gilt als bekannt.
Nach dem Wiederaufbau alle Service-Schlüssel rotieren (geführte Rotation) und alte widerrufen.
4. **Neu aufsetzen:** neuer Master-Key, neues `SSHKV_APP_SECRET`, frische Datenbank oder geprüftes
Backup; der Worker erzeugt einen neuen Deployment-Key; Hosts per Bootstrap neu aufnehmen.
5. **Protokoll auswerten:** Syslog-Kopie (unveränderlich außerhalb von sshkv) mit dem Protokoll
vergleichen; `protokoll_kette_pruefen()` zeigt manipulierte Zeilen.
> Eine eingebaute **Rotation des Deployment-Keys** gibt es noch nicht (siehe `docs/issues`). Bis dahin
> ist Schritt 2 Handarbeit.
## Betriebsregeln
- **Nur intern betreiben.** Die Weboberfläche gehört ins interne Netz oder hinter ein VPN, nie ins
Internet. Compose bindet standardmäßig nur an `127.0.0.1`; ein Reverse Proxy mit TLS steht davor
(siehe [installation.md](installation.md)).
- **Docker-Host wie einen Bastion-Host behandeln:** wenige Admins, SSH nur mit Schlüssel, Updates,
keine anderen Dienste.
- **Master-Key** getrennt sichern, Rechte 0400, nur für den Worker-Benutzer lesbar.
- **Syslog** einschalten (`SSHKV_SYSLOG_*`), damit eine Kopie des Protokolls außerhalb von sshkv liegt.
- **Hosts:** `PermitRootLogin prohibit-password` (sshkv zeigt das als Hinweis an), Anmeldeart
„Dienstbenutzer“ bevorzugen – dann kann root-Login ganz aus bleiben.
- **Benachrichtigungen** für Drift und geänderte Host-Keys aktivieren, damit Abweichungen auffallen.
+92
View File
@@ -0,0 +1,92 @@
# Sicherung und Wiederherstellung
Eine Sicherung von SSHKeyVault besteht aus **zwei Teilen, die getrennt aufbewahrt werden**:
| Teil | Inhalt | Wie oft | Wohin |
|---|---|---|---|
| **Datenbank** | Hosts, Schlüssel (Private Keys nur verschlüsselt), Zuweisungen, Protokoll, Einstellungen | täglich | normales Backup-Ziel (`/var/backups/sshkv`, von dort auf das Sicherungssystem) |
| **Master-Key** | 32 Byte, entschlüsselt alle Private Keys | einmalig nach der Installation und nach jeder Master-Key-Rotation | **getrennt:** Passwortmanager, Tresor, verschlüsselter Datenträger |
| `.env` | `SSHKV_APP_SECRET`, OIDC-Zugang, Datenbankpasswort | nach jeder Änderung | wie der Master-Key |
> **Ohne Master-Key ist jedes Datenbank-Backup wertlos** – die Private Keys lassen sich nicht mehr
> entschlüsseln. **Mit Master-Key neben dem Backup** gibt ein gestohlenes Backup alle Private Keys
> preis. Deshalb niemals beides am selben Ort.
>
> Ohne das richtige `SSHKV_APP_SECRET` gehen TOTP des Notfall-Admins, SMTP-Passwort und
> Webhook-Geheimnis verloren (neu einrichten genügt); Private Keys hängen nur am Master-Key.
## Sichern
```bash
cd /opt/sshkv # Verzeichnis mit docker-compose.yml und .env
sudo bash deploy/sichern.sh # nach /var/backups/sshkv
sudo bash deploy/sichern.sh /mnt/sicherung/sshkv
```
Das Skript
- schreibt `datenbank-<Stand>.sql.gz` (`pg_dump --clean --if-exists`, nur das Schema `public` – die
Warteschlange legt der Worker selbst neu an),
- legt `master-key-pruefwert-<Stand>.txt` daneben: den SHA-256 der Master-Key-Datei. Er verrät nichts
über den Schlüssel, zeigt beim Zurückspielen aber, ob der richtige bereitliegt,
- behält die letzten 14 Sicherungen (`SSHKV_SICHERUNGEN_BEHALTEN`),
- sichert den Master-Key **nicht** mit.
Als täglicher Cronjob auf dem Docker-Host:
```cron
15 3 * * * root cd /opt/sshkv && bash deploy/sichern.sh >/var/log/sshkv-sicherung.log 2>&1
```
## Master-Key sichern (einmalig)
```bash
sudo base64 /opt/sshkv/geheim/master.key # Ausgabe in den Passwortmanager, Eintrag „sshkv Master-Key“
sudo sha256sum /opt/sshkv/geheim/master.key # Prüfwert dazuschreiben
```
Zurück in eine Datei: `echo '<base64>' | base64 -d > geheim/master.key && chmod 400 geheim/master.key`
und den Eigentümer setzen (Container-Benutzer 1001 oder `SSHKV_WORKER_USER`).
## Wiederherstellen
```bash
cd /opt/sshkv
sudo bash deploy/wiederherstellen.sh /var/backups/sshkv # jüngste Sicherung
sudo bash deploy/wiederherstellen.sh /var/backups/sshkv 20261012-163958
```
Das Skript
1. prüft den Master-Key unter `SSHKV_MASTER_KEY_HOST_FILE` gegen den Prüfwert der Sicherung und
**bricht ab, wenn er nicht passt**,
2. fragt nach (`--ja` überspringt),
3. hält Weboberfläche und Worker an,
4. spielt die Datenbank **in einer Transaktion** zurück – scheitert etwas, bleibt der alte Stand
vollständig erhalten,
5. setzt die Warteschlange zurück und bricht offene Vorgänge ab (ihre Jobs gehören nicht zur
Sicherung),
6. startet die Dienste und prüft, dass der Worker den Master-Key akzeptiert.
Zusätzlich prüft der **Worker bei jedem Start**, ob er den Deployment-Key mit dem eingebundenen
Master-Key entschlüsseln kann, und startet sonst nicht („Der Master-Key passt nicht zur Datenbank“).
### Geprobt am 12.10.2026
Auf einem Wegwerf-Stapel (gleiches `docker-compose.yml`):
1. Markierung angelegt (Host `srv-probe`, Protokolleintrag), gesichert.
2. Markierung gelöscht, weiterer Protokolleintrag geschrieben.
3. Zurückgespielt → Host wieder da, Protokoll wieder auf dem gesicherten Stand, Hash-Kette
unversehrt (`protokoll_kette_pruefen()`), Weboberfläche gesund, Worker bereit.
4. Mit **falschem Master-Key** zurückgespielt → Abbruch vor jeder Änderung.
Dabei fielen zwei Fehler auf, die jetzt behoben sind: Das Schema der Warteschlange ließ sich mit
`--clean` nicht zurückspielen, und ein Abbruch ohne Transaktion hatte es halb gelöscht.
## Nach einer Wiederherstellung
- **„Jetzt abgleichen“** auf der Übersicht: Der Stand der Hosts kann neuer sein als die Sicherung.
- Zuweisungen, die nach der Sicherung ausgerollt wurden, kennt sshkv nicht mehr – der Abgleich zeigt
sie als unbekannte Schlüssel; übernehmen oder entfernen.
- Wurde nach der Sicherung ein Schlüssel widerrufen, steht er wieder als aktiv da: erneut widerrufen.