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.
This commit is contained in:
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user