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:
@@ -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`) |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user