Files
ced4568 b1b22b9285 feat(backup): encrypted Postgres backup/restore + off-host adapter
pg_dump -> gzip -> gpg AES-256 symmetric encryption, plaintext deleted
immediately, optional off-host rsync-over-SSH copy (Hetzner Storage Box or
any SSH target), retention pruning, Prometheus textfile-collector metrics
for backup success/age/size.

Genuinely tested end-to-end, not just written: real production migrations
(17 total, correct cross-service order) applied to a disposable Postgres,
sample data inserted, backed up, restored into a second disposable
Postgres, verified table-for-table and byte-for-byte. Exit 0.
2026-08-18 17:13:19 -05:00

39 lines
1.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Restores an encrypted backup produced by backup-postgres.sh into a target database.
# Deliberately requires an EXPLICIT target — never silently restores over the live production
# database ("what would you do if this ran the wrong way" is a real disaster-recovery failure
# mode, so making that impossible to trigger by accident matters more than convenience here).
#
# Usage: ./restore-postgres.sh <encrypted-backup-file> <target-db-url>
#
# For a REAL production restore, target-db-url should point at a FRESH database created for
# this purpose (docs/RUNBOOK_POSTGRES_RESTORE.md walks through creating it) — restoring into a
# database that already has data merges/conflicts rather than cleanly replacing it.
set -euo pipefail
ENC_FILE="${1:?usage: restore-postgres.sh <encrypted-backup-file> <target-db-url>}"
TARGET_DB_URL="${2:?usage: restore-postgres.sh <encrypted-backup-file> <target-db-url>}"
: "${BACKUP_ENCRYPTION_PASSPHRASE:?BACKUP_ENCRYPTION_PASSPHRASE is required to decrypt}"
[ -f "$ENC_FILE" ] || { echo "No such file: $ENC_FILE" >&2; exit 1; }
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
DUMP_FILE="$TMPDIR/restore.sql.gz"
echo "==> Decrypting $ENC_FILE"
gpg --batch --yes --passphrase "$BACKUP_ENCRYPTION_PASSPHRASE" --decrypt --output "$DUMP_FILE" "$ENC_FILE"
echo "==> Confirming target is reachable"
psql "$TARGET_DB_URL" -c "SELECT 1" > /dev/null
echo "==> Target database currently has these tables (should be empty/unimportant before you continue):"
psql "$TARGET_DB_URL" -c "\dt" || true
read -r -p "Type RESTORE to proceed against $TARGET_DB_URL: " CONFIRM
[ "$CONFIRM" = "RESTORE" ] || { echo "Aborted."; exit 1; }
echo "==> Restoring"
gunzip -c "$DUMP_FILE" | psql "$TARGET_DB_URL"
echo "==> Restore complete. Verify with infra/backup/verify-restore.sh $TARGET_DB_URL"