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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 16:46:38 +02:00
co-authored by Claude Opus 5.5
parent f791816e85
commit 3b28f73c95
13 changed files with 682 additions and 3 deletions
+67
View File
@@ -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."
+80
View File
@@ -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")."