Merge branch 'phase/029-handbuch'
# Conflicts: # src/worker/index.ts
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,216 @@
|
|||||||
|
# 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)
|
||||||
|
11. [Audit-Log](#11-audit-log)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 11. Audit-Log
|
||||||
|
|
||||||
|
Jede Aktion steht im **Audit-Log**: Zeitpunkt, Benutzer, Host, Zielbenutzer, Fingerprint, das Diff
|
||||||
|
der geänderten Datei und das Ergebnis – nie ein Private Key oder Passwort. Einträge lassen sich
|
||||||
|
weder ändern noch löschen; jeder Eintrag ist per Hash mit seinem Vorgänger verkettet.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Filter** nach Zeitraum, Aktionsgruppe, Benutzer, Host, Ergebnis und Freitext; eine Zeile
|
||||||
|
aufklappen zeigt Diff, Details und Hashes.
|
||||||
|
- **Kette prüfen** rechnet alle Hashes nach. Wurde eine Zeile an sshkv vorbei in der Datenbank
|
||||||
|
verändert, zeigt die Seite die erste betroffene Zeile.
|
||||||
|
- **CSV-Export** mit denselben Filtern (der Export selbst wird protokolliert).
|
||||||
|
- **Syslog:** Mit `SSHKV_SYSLOG_HOST` geht jeder Eintrag zusätzlich an einen zentralen Syslog-Server
|
||||||
|
(z. B. für Zabbix) – eine Kopie außerhalb von sshkv.
|
||||||
|
|
||||||
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,113 @@
|
|||||||
|
# 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 (RFC 5424, UDP oder TCP) |
|
||||||
|
| `SSHKV_SYSLOG_FACILITY` / `_APPNAME` | Facility (Standard `authpriv`) und App-Name (Standard `sshkv`) |
|
||||||
|
| `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
@@ -13,7 +13,7 @@ import {
|
|||||||
import { vorgangAusfuehren } from "@/worker/ausfuehren";
|
import { vorgangAusfuehren } from "@/worker/ausfuehren";
|
||||||
import { abholungenAufraeumen } from "@/worker/ablaeufe/privatekey";
|
import { abholungenAufraeumen } from "@/worker/ablaeufe/privatekey";
|
||||||
import { taktVersenden, zusammenfassungVersenden } from "@/worker/benachrichtigung/versand";
|
import { taktVersenden, zusammenfassungVersenden } from "@/worker/benachrichtigung/versand";
|
||||||
import { deploymentKeySicherstellen } from "@/worker/geheimnisse";
|
import { deploymentKeySicherstellen, masterKeyPruefen } from "@/worker/geheimnisse";
|
||||||
import { HANDLER } from "@/worker/handler";
|
import { HANDLER } from "@/worker/handler";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,7 +23,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