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:
@@ -18,7 +18,15 @@ SSHKeyVault hat **root-Zugriff auf alle verwalteten Hosts**. Deshalb:
|
|||||||
- Private Keys liegen nur verschlüsselt in der Datenbank (AES-256-GCM, Envelope Encryption);
|
- Private Keys liegen nur verschlüsselt in der Datenbank (AES-256-GCM, Envelope Encryption);
|
||||||
entschlüsseln kann ausschließlich der Worker, der Webprozess hat den Master-Key nicht.
|
entschlüsseln kann ausschließlich der Worker, der Webprozess hat den Master-Key nicht.
|
||||||
|
|
||||||
Mehr dazu in [`docs/PLAN.md`](docs/PLAN.md), Abschnitt 3.
|
Mehr dazu in [`docs/betrieb/sicherheit.md`](docs/betrieb/sicherheit.md) (mit Bedrohungsanalyse und
|
||||||
|
Notfallplan) und [`docs/betrieb/sicherung.md`](docs/betrieb/sicherung.md).
|
||||||
|
|
||||||
|
## Dokumentation
|
||||||
|
|
||||||
|
- [Handbuch](docs/anleitung/README.md) – Bedienung Schritt für Schritt mit Bildern
|
||||||
|
- [Installation und Betrieb](docs/betrieb/installation.md) – Docker Compose, authentik, Reverse Proxy
|
||||||
|
- [Sicherung und Wiederherstellung](docs/betrieb/sicherung.md) – `deploy/sichern.sh`, `deploy/wiederherstellen.sh`
|
||||||
|
- [Sicherheit](docs/betrieb/sicherheit.md) – Aufbau, Bedrohungsanalyse, Notfallplan
|
||||||
|
|
||||||
## Installation (Docker Compose)
|
## Installation (Docker Compose)
|
||||||
|
|
||||||
|
|||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SSHKeyVault sichern (#029).
|
||||||
|
#
|
||||||
|
# bash deploy/sichern.sh # nach /var/backups/sshkv
|
||||||
|
# bash deploy/sichern.sh /mnt/sicherung/sshkv # anderes Ziel
|
||||||
|
#
|
||||||
|
# Sichert die Datenbank (pg_dump mit --clean, damit sie sich auch in eine nicht leere
|
||||||
|
# Datenbank zurueckspielen laesst) und legt einen Pruefwert des Master-Keys daneben.
|
||||||
|
#
|
||||||
|
# Den MASTER-KEY selbst sichert dieses Skript absichtlich NICHT mit: Datenbank und Master-Key
|
||||||
|
# zusammen geben alle Private Keys preis. Den Master-Key einmalig und GETRENNT aufbewahren
|
||||||
|
# (Passwortmanager, Tresor, zweites Medium) – ohne ihn ist jede Sicherung wertlos.
|
||||||
|
# Siehe docs/betrieb/sicherung.md.
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
VERZEICHNIS=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
cd "$VERZEICHNIS"
|
||||||
|
|
||||||
|
gruen() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }
|
||||||
|
gelb() { printf '\033[1;33m! %s\033[0m\n' "$*"; }
|
||||||
|
fehler() {
|
||||||
|
printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ZIEL="${1:-/var/backups/sshkv}"
|
||||||
|
BEHALTEN="${SSHKV_SICHERUNGEN_BEHALTEN:-14}"
|
||||||
|
STAND=$(date +%Y%m%d-%H%M%S)
|
||||||
|
|
||||||
|
mkdir -p "$ZIEL"
|
||||||
|
chmod 0700 "$ZIEL"
|
||||||
|
|
||||||
|
# Datenbank: als Klartext-SQL, gzip-komprimiert. Enthaelt nur verschluesselte Private Keys.
|
||||||
|
# Nur das Schema public: Die Warteschlange (Schema pgboss) legt der Worker beim Start neu an,
|
||||||
|
# und ihre partitionierten Tabellen lassen sich mit --clean nicht zurueckspielen (in der
|
||||||
|
# Wiederherstellungsprobe aufgefallen).
|
||||||
|
docker compose exec -T db pg_dump -U sshkv --clean --if-exists --no-owner --schema=public sshkv \
|
||||||
|
| gzip -9 > "${ZIEL}/datenbank-${STAND}.sql.gz" \
|
||||||
|
|| fehler "pg_dump ist fehlgeschlagen. Läuft der Dienst db?"
|
||||||
|
[ -s "${ZIEL}/datenbank-${STAND}.sql.gz" ] || fehler "Die Sicherung ist leer."
|
||||||
|
chmod 0600 "${ZIEL}/datenbank-${STAND}.sql.gz"
|
||||||
|
gruen "Datenbank gesichert: ${ZIEL}/datenbank-${STAND}.sql.gz"
|
||||||
|
|
||||||
|
# Pruefwert des Master-Keys: verraet nichts ueber den Schluessel, zeigt beim Zurueckspielen
|
||||||
|
# aber, ob der richtige Master-Key bereitliegt.
|
||||||
|
ENV_DATEI="${SSHKV_ENV_DATEI:-.env}"
|
||||||
|
SCHLUESSEL="$(grep -E '^SSHKV_MASTER_KEY_HOST_FILE=' "$ENV_DATEI" 2>/dev/null | cut -d= -f2- || true)"
|
||||||
|
SCHLUESSEL="${SCHLUESSEL:-./geheim/master.key}"
|
||||||
|
if [ -r "$SCHLUESSEL" ]; then
|
||||||
|
sha256sum "$SCHLUESSEL" | cut -d' ' -f1 > "${ZIEL}/master-key-pruefwert-${STAND}.txt"
|
||||||
|
chmod 0600 "${ZIEL}/master-key-pruefwert-${STAND}.txt"
|
||||||
|
gruen "Prüfwert des Master-Keys abgelegt (nicht der Schlüssel selbst)."
|
||||||
|
else
|
||||||
|
gelb "Master-Key ${SCHLUESSEL} nicht lesbar – kein Prüfwert abgelegt (mit sudo ausführen?)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Alte Sicherungen aufraeumen (samt Pruefwert).
|
||||||
|
ls -1t "${ZIEL}"/datenbank-*.sql.gz 2>/dev/null | tail -n +"$((BEHALTEN + 1))" | while read -r alt; do
|
||||||
|
alter_stand=$(basename "$alt" .sql.gz)
|
||||||
|
alter_stand=${alter_stand#datenbank-}
|
||||||
|
rm -f -- "$alt" "${ZIEL}/master-key-pruefwert-${alter_stand}.txt"
|
||||||
|
done
|
||||||
|
|
||||||
|
gelb "Den Master-Key getrennt aufbewahren – er gehört NICHT neben diese Sicherung."
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SSHKeyVault aus einer Sicherung wiederherstellen (#029).
|
||||||
|
#
|
||||||
|
# bash deploy/wiederherstellen.sh /var/backups/sshkv # jüngste Sicherung
|
||||||
|
# bash deploy/wiederherstellen.sh /var/backups/sshkv 20261012-170000
|
||||||
|
# bash deploy/wiederherstellen.sh … --ja # ohne Rückfrage
|
||||||
|
#
|
||||||
|
# Voraussetzung: Der zur Sicherung gehoerende Master-Key liegt an der in .env genannten Stelle
|
||||||
|
# (SSHKV_MASTER_KEY_HOST_FILE). Das Skript vergleicht ihn mit dem Pruefwert der Sicherung und
|
||||||
|
# bricht ab, wenn er nicht passt – sonst laeuft sshkv ohne lesbare Private Keys an.
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
VERZEICHNIS=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
cd "$VERZEICHNIS"
|
||||||
|
|
||||||
|
blau() { printf '\033[1;36m› %s\033[0m\n' "$*"; }
|
||||||
|
gruen() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }
|
||||||
|
fehler() {
|
||||||
|
printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
QUELLE="${1:-/var/backups/sshkv}"
|
||||||
|
STAND=""
|
||||||
|
OHNE_RUECKFRAGE="nein"
|
||||||
|
for argument in "${@:2}"; do
|
||||||
|
if [ "$argument" = "--ja" ]; then OHNE_RUECKFRAGE="ja"; else STAND="$argument"; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -d "$QUELLE" ] || fehler "Kein Sicherungsordner unter ${QUELLE}."
|
||||||
|
if [ -z "$STAND" ]; then
|
||||||
|
DATENBANK=$(ls -1t "${QUELLE}"/datenbank-*.sql.gz 2>/dev/null | head -n1 || true)
|
||||||
|
else
|
||||||
|
DATENBANK="${QUELLE}/datenbank-${STAND}.sql.gz"
|
||||||
|
fi
|
||||||
|
[ -n "${DATENBANK:-}" ] && [ -f "$DATENBANK" ] || fehler "Keine Datenbanksicherung in ${QUELLE} gefunden."
|
||||||
|
PRUEFWERT="${DATENBANK/datenbank-/master-key-pruefwert-}"
|
||||||
|
PRUEFWERT="${PRUEFWERT/.sql.gz/.txt}"
|
||||||
|
|
||||||
|
ENV_DATEI="${SSHKV_ENV_DATEI:-.env}"
|
||||||
|
SCHLUESSEL="$(grep -E '^SSHKV_MASTER_KEY_HOST_FILE=' "$ENV_DATEI" 2>/dev/null | cut -d= -f2- || true)"
|
||||||
|
SCHLUESSEL="${SCHLUESSEL:-./geheim/master.key}"
|
||||||
|
if [ -f "$PRUEFWERT" ]; then
|
||||||
|
[ -r "$SCHLUESSEL" ] || fehler "Master-Key ${SCHLUESSEL} nicht lesbar (mit sudo ausführen?)."
|
||||||
|
[ "$(sha256sum "$SCHLUESSEL" | cut -d' ' -f1)" = "$(cat "$PRUEFWERT")" ] \
|
||||||
|
|| fehler "Der Master-Key unter ${SCHLUESSEL} gehört NICHT zu dieser Sicherung. Abbruch."
|
||||||
|
gruen "Master-Key passt zur Sicherung."
|
||||||
|
else
|
||||||
|
printf '\033[1;33m! Kein Prüfwert zur Sicherung – der Worker prüft den Master-Key beim Start.\033[0m\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\033[1;33m! Achtung: Der aktuelle Datenbestand wird durch die Sicherung ersetzt.\033[0m\n'
|
||||||
|
echo " Sicherung: $(basename "$DATENBANK")"
|
||||||
|
if [ "$OHNE_RUECKFRAGE" != "ja" ]; then
|
||||||
|
read -r -p "Wirklich zurückspielen? [tippen Sie ja] " antwort
|
||||||
|
[ "$antwort" = "ja" ] || fehler "Abgebrochen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
blau "Weboberfläche und Worker anhalten …"
|
||||||
|
docker compose stop web worker
|
||||||
|
blau "Datenbank zurückspielen …"
|
||||||
|
# Eine Transaktion: scheitert etwas, bleibt der alte Stand vollstaendig erhalten (in der
|
||||||
|
# Wiederherstellungsprobe hatte ein Abbruch ohne Transaktion das Schema pgboss halb geloescht).
|
||||||
|
gunzip -c "$DATENBANK" | docker compose exec -T db psql -U sshkv -d sshkv -v ON_ERROR_STOP=1 --single-transaction -q >/dev/null \
|
||||||
|
|| fehler "Zurückspielen fehlgeschlagen. Web und Worker sind angehalten – nach Klärung: docker compose up -d"
|
||||||
|
# Die Warteschlange gehoert nicht zur Sicherung: frisch anlegen lassen (macht der Worker beim
|
||||||
|
# Start), offene Vorgaenge abbrechen – ihre Jobs sind damit weg.
|
||||||
|
docker compose exec -T db psql -U sshkv -d sshkv -q -c "DROP SCHEMA IF EXISTS pgboss CASCADE" >/dev/null
|
||||||
|
docker compose exec -T db psql -U sshkv -d sshkv -q -c \
|
||||||
|
"UPDATE \"Vorgang\" SET status = 'ABGEBROCHEN', fehler = 'Durch Wiederherstellung abgebrochen.', \"beendetAm\" = now() WHERE status IN ('WARTEND','LAEUFT')" >/dev/null
|
||||||
|
blau "Dienste starten …"
|
||||||
|
docker compose up -d --wait web worker
|
||||||
|
sleep 3
|
||||||
|
if docker compose logs --tail 20 worker 2>&1 | grep -q "passt nicht zur Datenbank"; then
|
||||||
|
fehler "Der Worker meldet: Master-Key passt nicht zur Datenbank. Siehe docker compose logs worker."
|
||||||
|
fi
|
||||||
|
gruen "Wiederhergestellt aus $(basename "$DATENBANK")."
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# SSHKeyVault – Handbuch
|
||||||
|
|
||||||
|
SSHKeyVault (`sshkv`) verwaltet SSH-Schlüssel zentral: Schlüsselpaare erzeugen, Public Keys in
|
||||||
|
`authorized_keys` verteilen, Private Keys gezielt auf andere Hosts legen, Ist und Soll abgleichen –
|
||||||
|
alles protokolliert. Installation und Betrieb: [../betrieb/installation.md](../betrieb/installation.md),
|
||||||
|
Sicherung: [../betrieb/sicherung.md](../betrieb/sicherung.md), Sicherheit:
|
||||||
|
[../betrieb/sicherheit.md](../betrieb/sicherheit.md).
|
||||||
|
|
||||||
|
**Grundsatz:** Auf einem Host ändert sich nur etwas, nachdem Sie einen **Dry-Run** gesehen und
|
||||||
|
„Änderung anwenden“ geklickt haben. Alles, was sshkv in `authorized_keys`, `~/.ssh/config` und
|
||||||
|
`known_hosts` schreibt, steht in einem verwalteten Block:
|
||||||
|
|
||||||
|
```
|
||||||
|
# BEGIN SSHKV MANAGED – nicht manuell bearbeiten
|
||||||
|
…
|
||||||
|
# END SSHKV MANAGED
|
||||||
|
```
|
||||||
|
|
||||||
|
Zeilen außerhalb dieses Blocks bleiben unverändert – außer Sie übernehmen einen Schlüssel ausdrücklich
|
||||||
|
in die Verwaltung.
|
||||||
|
|
||||||
|
1. [Anmelden](#1-anmelden)
|
||||||
|
2. [Übersicht](#2-übersicht)
|
||||||
|
3. [Einen Host aufnehmen](#3-einen-host-aufnehmen)
|
||||||
|
4. [Kern-Anwendungsfall: Host A meldet sich bei Host B an](#4-kern-anwendungsfall-host-a-meldet-sich-bei-host-b-an)
|
||||||
|
5. [Deployment-Zugang für Claude Code mit dem Assistenten](#5-deployment-zugang-für-claude-code-mit-dem-assistenten)
|
||||||
|
6. [Abgleich, Drift und Übernahme](#6-abgleich-drift-und-übernahme)
|
||||||
|
7. [Rotation und Widerruf](#7-rotation-und-widerruf)
|
||||||
|
8. [Personen, Self-Service und Zuweisungsmatrix](#8-personen-self-service-und-zuweisungsmatrix)
|
||||||
|
9. [Vorlagen](#9-vorlagen)
|
||||||
|
10. [Private Key anzeigen oder löschen](#10-private-key-anzeigen-oder-löschen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Anmelden
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Mit authentik anmelden** – der Normalfall. Ihre Rolle (Leser, Operator, Admin) kommt aus den
|
||||||
|
authentik-Gruppen und wird bei jeder Anmeldung neu bestimmt.
|
||||||
|
- **Notfallzugang** – nur, wenn authentik ausfällt. Anmeldename, Passwort und ein Code aus einer
|
||||||
|
Authenticator-App. Bei der ersten Anmeldung richtet der Notfall-Admin den zweiten Faktor ein
|
||||||
|
(QR-Code scannen, Code bestätigen).
|
||||||
|
|
||||||
|
Kritische Aktionen (Private Key anzeigen, Benutzer mit sudo anlegen, Massenwiderruf, Host-Key
|
||||||
|
ersetzen) verlangen eine **erneute Bestätigung**; sie gilt fünf Minuten.
|
||||||
|
|
||||||
|
## 2. Übersicht
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Kennzahlen, Hosts mit Abweichung, bald ablaufende Schlüssel und die letzten Aktionen. Von hier aus:
|
||||||
|
**Jetzt abgleichen** (alle Hosts), **Host-zu-Host-Assistent**, **Neuer Schlüssel**.
|
||||||
|
|
||||||
|
## 3. Einen Host aufnehmen
|
||||||
|
|
||||||
|
**Hosts → + Host anlegen:** Name (z. B. `srv-pve01`), Adresse, Port, Gruppen, Tags. Bei
|
||||||
|
**Anmeldung** ist „Dienstbenutzer `sshkv` mit sudo“ empfohlen – im auth.log des Hosts ist dann
|
||||||
|
eindeutig zu sehen, was von sshkv kam, und root-Login kann aus bleiben.
|
||||||
|
|
||||||
|
**Host-Key bestätigen.** „Host-Key abrufen“ holt den Fingerprint, ohne sich anzumelden. Vergleichen
|
||||||
|
Sie ihn direkt am Host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Erst nach „Fingerprint stimmt – vertrauen“ verbindet sich sshkv. Ändert sich der Host-Key später,
|
||||||
|
bricht jede Verbindung ab und die Hostseite warnt deutlich – annehmen nur, wenn der Host wirklich neu
|
||||||
|
installiert wurde.
|
||||||
|
|
||||||
|
**Bootstrap.** Zwei Wege, den Deployment-Key von sshkv auf den Host zu bringen:
|
||||||
|
|
||||||
|
- **Einmalig per Passwort** (root oder ein Benutzer mit sudo). Das Passwort wird nicht gespeichert.
|
||||||
|
- **Per Befehl:** den angezeigten Befehl auf dem Host einfügen, danach „Einrichtung prüfen“.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Danach zeigt die Hostseite den **sshd-Befund**. Steht dort „root darf sich per Passwort anmelden“,
|
||||||
|
empfiehlt sich `PermitRootLogin prohibit-password` in `/etc/ssh/sshd_config` – sshkv ändert das nicht
|
||||||
|
von sich aus.
|
||||||
|
|
||||||
|
## 4. Kern-Anwendungsfall: Host A meldet sich bei Host B an
|
||||||
|
|
||||||
|
Beispiel: Der Backup-Server `srv-backup01` soll sich als `root` per SSH auf `srv-pve01` anmelden.
|
||||||
|
Beide Hosts sind aufgenommen (Kapitel 3).
|
||||||
|
|
||||||
|
**1. Schlüssel erzeugen.** *Schlüssel → + Schlüssel anlegen → Erzeugen*: Zweck „Backup srv-backup01 →
|
||||||
|
srv-pve01“, Typ ed25519, optional Ablaufdatum. Keine Passphrase – ein Cronjob kann sie nicht eingeben.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**2a. Private Key auf A.** In der Karte *Private Key ausgerollt auf* → **+ Ausrollen**: Quell-Host
|
||||||
|
`srv-backup01`, Benutzer `root`, Pfad `~/.ssh/id_ed25519_…`. Mit **Eintrag in ~/.ssh/config** und
|
||||||
|
Ziel-Host `srv-pve01` legt sshkv einen Alias an und trägt den (bereits bestätigten) Host-Key von B in
|
||||||
|
`known_hosts` von A ein. *HostName* ist die Adresse, unter der **A** den Host B erreicht.
|
||||||
|
|
||||||
|
**2b. Public Key auf B.** In der Karte *Public Key ausgerollt auf* → **+ Ausrollen**: Ziel
|
||||||
|
`srv-pve01`, Benutzer `root`, optional Einschränkungen wie `from="10.0.0.12",no-pty`.
|
||||||
|
|
||||||
|
2a und 2b sind unabhängig und jederzeit nachholbar.
|
||||||
|
|
||||||
|
**3. Dry-Run und anwenden.** *Dry-Run starten* zeigt je Host, was sich ändert – beim Private Key nur
|
||||||
|
„wird angelegt“, nie den Inhalt.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
„Änderung anwenden“ schreibt genau das, was Sie gesehen haben (mit Sicherung der Dateien). Hat sich
|
||||||
|
in der Zwischenzeit etwas auf dem Host geändert, bricht sshkv ab – dann den Dry-Run neu starten.
|
||||||
|
|
||||||
|
**4. Verbindung testen.** In der Private-Karte „Verbindung testen“ ruft auf A
|
||||||
|
`ssh -o BatchMode=yes <alias> true` auf – genau das, was später der Cronjob braucht.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 5. Deployment-Zugang für Claude Code mit dem Assistenten
|
||||||
|
|
||||||
|
Szenario: Claude Code auf `srv-rz-claude-linux` soll eine Anwendung per SSH auf `srv-app01`
|
||||||
|
ausrollen. **Übersicht → Host-zu-Host-Assistent** (nur Admins):
|
||||||
|
|
||||||
|
1. **Vorlage „Deployment-Zugang“** wählen, Zweck „Claude Code Deployment → srv-app01“. Die Vorlage
|
||||||
|
setzt Benutzer `deploy`, 90 Tage Laufzeit, `from="<IP von A>"` und den Alias `deploy-<Ziel>`.
|
||||||
|
2. **Quelle:** `srv-rz-claude-linux`, Benutzer, unter dem Claude Code läuft.
|
||||||
|
3. **Ziel:** `srv-app01`, Benutzer `deploy` – „Benutzer anlegen“ ist vorgewählt; sudo nach Bedarf
|
||||||
|
(keine, bestimmte Befehle mit Vorschau der sudoers-Datei, oder voll). Fingerprint von B bestätigen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. **Prüfen** und „Vorbereiten“: sshkv erzeugt den Schlüssel, legt `deploy` an (frische Bestätigung
|
||||||
|
nötig – sie öffnet sich in einem neuen Tab, Ihre Eingaben bleiben stehen) und legt die
|
||||||
|
Zuweisungen an.
|
||||||
|
5. **Ausrollen:** Dry-Run, anwenden, „Verbindung von A nach B testen“.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Claude Code kann danach einfach `ssh deploy-srv-app01` benutzen. Wählen Sie `root` als Ziel, warnt
|
||||||
|
der Assistent: Ein KI-Agent hätte dann uneingeschränkten Zugriff.
|
||||||
|
|
||||||
|
**Nach Projektende** (oder Ablaufdatum): auf der Schlüsselseite **Widerrufen** – entfernt Public und
|
||||||
|
Private Key, config- und known_hosts-Eintrag und auf Wunsch den Benutzer `deploy` samt sudoers-Datei.
|
||||||
|
|
||||||
|
## 6. Abgleich, Drift und Übernahme
|
||||||
|
|
||||||
|
Nachts um 02:30 (einstellbar) und auf Knopfdruck vergleicht sshkv jeden Host mit dem Soll. Gefunden
|
||||||
|
werden fehlende Schlüssel, von Hand geänderte Blöcke, **unbekannte Schlüssel** außerhalb des Blocks,
|
||||||
|
falsche Rechte von `~/.ssh` und veränderte Private Keys. Der Abgleich ändert auf dem Host nichts.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Ein unbekannter Schlüssel lässt sich **übernehmen**: nur als bekannt aufnehmen (der Befund
|
||||||
|
verschwindet) oder „in den verwalteten Block holen“ – dann wandert die Zeile beim nächsten Ausrollen
|
||||||
|
(mit Dry-Run) in den Block.
|
||||||
|
|
||||||
|
## 7. Rotation und Widerruf
|
||||||
|
|
||||||
|
**Rotieren** (Schlüsselseite): Ein neuer Schlüssel kommt an alle Stellen des alten, der Alias in
|
||||||
|
`~/.ssh/config` zeigt auf die neue Datei, die Verbindungen werden getestet – **erst dann** wird der
|
||||||
|
alte entfernt. Die Rotation lässt sich jederzeit fortsetzen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Widerrufen** (Schlüsselseite, Gefahr-Knopf): Der Dialog zeigt, was entfernt wird; danach Dry-Run
|
||||||
|
und anwenden. Ist ein Host nicht erreichbar, bleibt der Schlüssel als „Widerruf unvollständig“
|
||||||
|
markiert, bis auch dort alles entfernt ist. **Massenwiderruf:** in der Schlüsselliste mehrere
|
||||||
|
auswählen (nur Admin).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 8. Personen, Self-Service und Zuweisungsmatrix
|
||||||
|
|
||||||
|
- **Personen:** Admins, Mitarbeiter, Dienstleister mit ihren Schlüsseln. Private Keys von Personen
|
||||||
|
speichert sshkv nie.
|
||||||
|
- **Meine Schlüssel:** Jede angemeldete Person kann eigene Public Keys hinterlegen. Wirksam werden
|
||||||
|
sie erst, wenn ein Operator oder Admin sie einem Host zuweist.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Zuweisungen:** Matrix Schlüssel × Host mit Filtern; ein Klick auf eine Zelle zeigt die
|
||||||
|
Zuweisungen und erlaubt neue. Export als CSV.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 9. Vorlagen
|
||||||
|
|
||||||
|
Vorlagen legen Schlüsseltyp, Laufzeit, Zielbenutzer (samt Anlegen und sudo), `authorized_keys`-
|
||||||
|
Optionen und das Alias-Muster vor. Platzhalter: `{{quellHost}}`, `{{quellHostIp}}`, `{{zielHost}}`,
|
||||||
|
`{{zielHostIp}}`. Eingebaut: „Deployment-Zugang“, „Backup-Zugang“, „Admin-Zugang Person“.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 10. Private Key anzeigen oder löschen
|
||||||
|
|
||||||
|
Nur Admins, nur nach erneuter Bestätigung: „anzeigen (Re-Auth)“ in der Info-Leiste eines Schlüssels.
|
||||||
|
Der Key wird genau einmal ausgeliefert und nirgends zwischengespeichert; jede Anzeige steht im
|
||||||
|
Protokoll. **Private Key löschen** entfernt ihn endgültig aus sshkv – auf den Hosts bleibt er; neu
|
||||||
|
ausrollen geht danach nicht mehr.
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -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.
|
||||||
@@ -219,3 +219,21 @@ export async function deploymentKeySicherstellen(): Promise<string> {
|
|||||||
});
|
});
|
||||||
return neu.id;
|
return neu.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passt der Master-Key zur Datenbank? (#029) Oeffnet probeweise den Deployment-Key. Schlaegt
|
||||||
|
* das fehl, ist entweder die falsche Master-Key-Datei eingebunden oder eine Sicherung ohne den
|
||||||
|
* dazugehoerigen Master-Key zurueckgespielt worden – beides soll beim Start auffallen, nicht
|
||||||
|
* erst beim ersten Ausrollen.
|
||||||
|
*/
|
||||||
|
export async function masterKeyPruefen(deploymentKeyId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const klartext = await privateKeyLaden(deploymentKeyId);
|
||||||
|
klartext.fill(0);
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
"Der Master-Key passt nicht zur Datenbank: Der Deployment-Key lässt sich nicht entschlüsseln. " +
|
||||||
|
"Ist SSHKV_MASTER_KEY_FILE die Datei, die zu dieser Datenbank gehört (Sicherung)?",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@/server/vorgaenge/warteschlange";
|
} from "@/server/vorgaenge/warteschlange";
|
||||||
import { vorgangAusfuehren } from "@/worker/ausfuehren";
|
import { vorgangAusfuehren } from "@/worker/ausfuehren";
|
||||||
import { abholungenAufraeumen } from "@/worker/ablaeufe/privatekey";
|
import { abholungenAufraeumen } from "@/worker/ablaeufe/privatekey";
|
||||||
import { deploymentKeySicherstellen } from "@/worker/geheimnisse";
|
import { deploymentKeySicherstellen, masterKeyPruefen } from "@/worker/geheimnisse";
|
||||||
import { HANDLER } from "@/worker/handler";
|
import { HANDLER } from "@/worker/handler";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,7 +20,7 @@ import { HANDLER } from "@/worker/handler";
|
|||||||
async function starten(): Promise<void> {
|
async function starten(): Promise<void> {
|
||||||
// Zuerst: ohne lesbaren Master-Key und Deployment-Key kann der Worker nichts tun, und das
|
// Zuerst: ohne lesbaren Master-Key und Deployment-Key kann der Worker nichts tun, und das
|
||||||
// soll beim Start auffallen, nicht beim ersten Vorgang.
|
// soll beim Start auffallen, nicht beim ersten Vorgang.
|
||||||
await deploymentKeySicherstellen();
|
await masterKeyPruefen(await deploymentKeySicherstellen());
|
||||||
const boss = await warteschlangeHolen();
|
const boss = await warteschlangeHolen();
|
||||||
|
|
||||||
await boss.work<VorgangsJob>(
|
await boss.work<VorgangsJob>(
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ test("neuen Host einrichten", async ({ page }) => {
|
|||||||
await expect(page.getByRole("button", { name: "Fingerprint stimmt – vertrauen" })).toBeVisible({
|
await expect(page.getByRole("button", { name: "Fingerprint stimmt – vertrauen" })).toBeVisible({
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
|
await page.screenshot({ path: "docs/anleitung/bilder/hostkey-bestaetigen-desktop.png", fullPage: true });
|
||||||
await page.getByRole("button", { name: "Fingerprint stimmt – vertrauen" }).click();
|
await page.getByRole("button", { name: "Fingerprint stimmt – vertrauen" }).click();
|
||||||
await expect(page.getByText("Einmalig per Passwort")).toBeVisible({ timeout: 15_000 });
|
await expect(page.getByText("Einmalig per Passwort")).toBeVisible({ timeout: 15_000 });
|
||||||
await expect(page.getByLabel("Bootstrap-Befehl", { exact: true })).toContainText("SSHKV_BOOTSTRAP");
|
await expect(page.getByLabel("Bootstrap-Befehl", { exact: true })).toContainText("SSHKV_BOOTSTRAP");
|
||||||
|
|||||||
@@ -73,3 +73,22 @@ describe("Deployment-Key", () => {
|
|||||||
expect(dump).not.toContain(kern.slice(0, 40));
|
expect(dump).not.toContain(kern.slice(0, 40));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Master-Key-Pruefung beim Start (#029)", () => {
|
||||||
|
it("meldet einen falschen Master-Key verstaendlich", async () => {
|
||||||
|
const { masterKeyPruefen } = await import("@/worker/geheimnisse");
|
||||||
|
const id = await deploymentKeySicherstellen();
|
||||||
|
await expect(masterKeyPruefen(id)).resolves.toBeUndefined();
|
||||||
|
const falsch = path.join(verzeichnis, "falsch.key");
|
||||||
|
writeFileSync(falsch, randomBytes(32), { mode: 0o600 });
|
||||||
|
const richtig = process.env["SSHKV_MASTER_KEY_FILE"];
|
||||||
|
process.env["SSHKV_MASTER_KEY_FILE"] = falsch;
|
||||||
|
masterKeyVergessen();
|
||||||
|
try {
|
||||||
|
await expect(masterKeyPruefen(id)).rejects.toThrow(/passt nicht zur Datenbank/);
|
||||||
|
} finally {
|
||||||
|
process.env["SSHKV_MASTER_KEY_FILE"] = richtig;
|
||||||
|
masterKeyVergessen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user