#!/bin/bash set -euo pipefail # Authelia Restore Smoke Test # # Nicht-destruktiver Restore-Smoke-Test fuer Authelia. # - extrahiert die Authelia-Config aus dem produktiven Borg-Archiv # - erzeugt eine minimale Test-Konfiguration, die restaurierte Begleitdateien # wie users_database.yml nutzt, aber produktive externe Abhaengigkeiten # durch Test-Backends ersetzt # - importiert optional den shared-Postgres-Dump fuer Authelia # - validiert die Test-Konfiguration mit `authelia config validate` # - startet einen isolierten Authelia-Container ohne Traefik # - prueft den HTTP-Health-Endpunkt # - bereinigt anschliessend # # Produktive Authelia-Container, produktive Postgres-DB, produktive Secrets # und produktiver SMTP-Versand werden NICHT angefasst. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" . "$SCRIPT_DIR/common.sh" WHATIF=0 KEEP_DATA=0 for arg in "$@"; do case "$arg" in --what-if) WHATIF=1 ;; --keep-data) KEEP_DATA=1 ;; *) echo "Unknown argument: $arg" >&2; exit 1 ;; esac done RESTORE_ROOT="/mnt/user/backups/restore-lab/authelia" RESTORED_CONFIG_DIR="$RESTORE_ROOT/config" TEST_CONFIG_DIR="$RESTORE_ROOT/test-config" REPORT_ROOT="/mnt/user/backups/restore-reports" EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/authelia-extract" COMPOSE_FILE="$SCRIPT_DIR/authelia-compose.test.yml" REPORT_FILE="$REPORT_ROOT/authelia-$(date +%F).md" if [ "$WHATIF" -eq 1 ]; then cat < Test-Postgres (kein produktives Postgres erreicht) * notifier -> Filesystem (KEIN SMTP-Versand) * session -> lokaler Smoke ohne produktive Session-Secrets - Test endpoint: 127.0.0.1:19091/api/health (no Traefik, no public domain) Smoke-Test: - authelia config validate gegen test-config/configuration.yml - HTTP 200 von /api/health EOF exit 0 fi require_cmd docker require_cmd curl require_path "$BORG_PASSPHRASE_FILE_DEFAULT" require_path "$COMPOSE_FILE" RESTORE_SUCCESS=0 cleanup() { cleanup_compose "$COMPOSE_FILE" if [ "$RESTORE_SUCCESS" -ne 1 ]; then preserve_on_failure "authelia" "$RESTORE_ROOT" rm -rf "$EXTRACT_DIR" return fi if [ "$KEEP_DATA" -ne 1 ]; then rm -rf "$RESTORE_ROOT" fi rm -rf "$EXTRACT_DIR" } trap cleanup EXIT rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT" mkdir -p "$RESTORED_CONFIG_DIR" "$TEST_CONFIG_DIR" "$RESTORE_ROOT/postgres" "$RESTORE_ROOT/dumps/latest" archive="$(latest_archive_name)" repo="$(borg_repo_url)" if [ -z "$archive" ] || [ -z "$repo" ]; then echo "Could not resolve Borg repo/archive from borg-ui database" >&2 exit 1 fi # Stufe 1: Config aus Borg extrahieren borg_extract "/restore/authelia-extract" "local/appdata/authelia/config" if [ ! -d "$EXTRACT_DIR/local/appdata/authelia/config" ]; then echo "Authelia config path missing in Borg archive" >&2 exit 1 fi cp -a "$EXTRACT_DIR/local/appdata/authelia/config/." "$RESTORED_CONFIG_DIR/" # Stufe 2: optionalen Postgres-Dump extrahieren und ggf. einspielen dump_available=0 if borg_extract "/restore/authelia-extract" "local/borg-dumps/latest/postgresql17-authelia.dump" 2>/dev/null; then if [ -f "$EXTRACT_DIR/local/borg-dumps/latest/postgresql17-authelia.dump" ]; then mv "$EXTRACT_DIR/local/borg-dumps/latest/postgresql17-authelia.dump" \ "$RESTORE_ROOT/dumps/latest/postgresql17-authelia.dump" dump_available=1 fi fi # Stufe 3: Minimale Test-Konfiguration erzeugen. # Die restaurierte Originalkonfig bleibt als Diagnosematerial erhalten. Der # Smoke nutzt bewusst eine neu geschriebene Test-Config, damit keine produktiven # Blocks (SMTP, echtes Postgres, Session/JWT-Altkeys) hineinmergen koennen. ORIGINAL_CONFIG_FILE="$RESTORED_CONFIG_DIR/configuration.yml" TEST_CONFIG_FILE="$TEST_CONFIG_DIR/configuration.yml" if [ ! -f "$ORIGINAL_CONFIG_FILE" ]; then echo "configuration.yml missing in restored config dir" >&2 exit 1 fi # Kopiere alle Begleitdateien (z. B. users_database.yml) in einen separaten # Runtime-Mount. configuration.yml wird danach vollstaendig neu geschrieben. cp -a "$RESTORED_CONFIG_DIR/." "$TEST_CONFIG_DIR/" cp "$ORIGINAL_CONFIG_FILE" "$RESTORED_CONFIG_DIR/configuration.yml.original" cat > "$TEST_CONFIG_FILE" <<'YAML' --- # Minimal-Konfiguration nur fuer den Restore-Smoke. theme: dark server: address: tcp://0.0.0.0:9091 log: level: info authentication_backend: file: path: /config/users_database.yml password: algorithm: argon2id iterations: 3 key_length: 32 salt_length: 16 memory: 65536 parallelism: 4 access_control: # Authelia 4.39 verlangt: wenn KEINE Regeln gesetzt sind, muss default_policy # 'two_factor' oder 'one_factor' sein. 'bypass' ist als Default-Policy ohne # explizite Regeln nicht erlaubt. Fuer den Smoke ist das egal: /api/health # ist ein public Endpunkt und laeuft nicht durch access_control. default_policy: two_factor regulation: max_retries: 3 find_time: 2m ban_time: 5m totp: issuer: kaleschke.info period: 30 skew: 1 storage: postgres: address: tcp://restoretest-authelia-postgres:5432 database: authelia username: authelia # Passwort kommt ueber AUTHELIA_STORAGE_POSTGRES_PASSWORD ENV. notifier: disable_startup_check: true filesystem: filename: /config/notifier/notifications.txt session: cookies: - name: authelia_session_restoretest domain: kaleschke.info authelia_url: https://auth.kaleschke.info default_redirection_url: https://glance.kaleschke.info expiration: 1h inactivity: 5m identity_validation: reset_password: jwt_secret: restoretest-authelia-reset-password-jwt-secret-placeholder-64bytes jwt_lifespan: 5m jwt_algorithm: HS256 YAML mkdir -p "$TEST_CONFIG_DIR/notifier" chmod -R a+rwX "$TEST_CONFIG_DIR/notifier" # Stufe 4: Test-Postgres hochfahren docker compose -f "$COMPOSE_FILE" up -d restoretest-authelia-postgres >/dev/null until docker exec restoretest-authelia-postgres pg_isready -U authelia -d authelia >/dev/null 2>&1; do sleep 2 done # Stufe 5: optional Dump einspielen dump_status="skipped (no dump in archive)" if [ "$dump_available" -eq 1 ]; then restore_ok=0 for attempt in $(seq 1 12); do if docker exec -i restoretest-authelia-postgres \ pg_restore -U authelia -d authelia --clean --if-exists --no-owner --no-privileges \ < "$RESTORE_ROOT/dumps/latest/postgresql17-authelia.dump" 2>/tmp/authelia-pg-restore.err; then restore_ok=1 break fi if grep -qiE "starting up|shutting down|connection refused|database .* does not exist" /tmp/authelia-pg-restore.err; then sleep 5 continue fi cat /tmp/authelia-pg-restore.err >&2 exit 1 done if [ "$restore_ok" -ne 1 ]; then cat /tmp/authelia-pg-restore.err >&2 exit 1 fi dump_status="restored" fi # Stufe 6: config validate im Container-Kontext, gegen minimale Test-Config validate_status="ok" if ! docker run --rm \ -e AUTHELIA_SESSION_SECRET=restoretest-authelia-session-secret-placeholder-32 \ -e AUTHELIA_STORAGE_ENCRYPTION_KEY=restoretest-authelia-storage-enc-key-placeholder-32 \ -e AUTHELIA_STORAGE_POSTGRES_PASSWORD=restoretest-authelia-db \ -v "$TEST_CONFIG_DIR:/config" \ authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b \ authelia config validate --config /config/configuration.yml \ >/tmp/authelia-validate.log 2>&1; then validate_status="failed" cat /tmp/authelia-validate.log >&2 exit 1 fi # Stufe 7: Authelia-Container starten. Das Compose nutzt test-config als # /config-Mount mit isolierten Test-Backends. docker compose -f "$COMPOSE_FILE" up -d restoretest-authelia >/dev/null http_status="" for _ in $(seq 1 60); do http_status="$(curl -s -o /tmp/authelia-body.html -w '%{http_code}' \ http://127.0.0.1:19091/api/health || true)" if [ "$http_status" = "200" ]; then break fi sleep 2 done if [ "$http_status" != "200" ]; then echo "Authelia HTTP health failed: status=$http_status" >&2 docker logs --tail 120 restoretest-authelia >&2 || true exit 1 fi write_report "$REPORT_FILE" < $REPORT_FILE"