* HH-445: deploy production observability and runbooks * fix(ops): share production database DSN * fix(HH-445): enforce database TLS gate * fix(HH-445): preserve production serve command * fix(prod): require external database dependencies * fix(prod): unify database host rejection gates * test(prod): enforce exact database TLS runbook contract --------- Co-authored-by: Rogee <rogee@ipao.vip>
74 lines
3.6 KiB
Bash
Executable File
74 lines
3.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Restore an encrypted GoChat backup into an empty database/storage target and
|
|
# print auditable RPO/RTO and business-integrity evidence.
|
|
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
"$script_dir/database_client_entrypoint.sh" --check
|
|
|
|
bundle=${1:?usage: db_restore.sh /path/to/gochat-*.tar.enc}
|
|
dsn=${GOCHAT_DATABASE_DSN:?GOCHAT_DATABASE_DSN is required}
|
|
storage=${GOCHAT_STORAGE_PATH:?GOCHAT_STORAGE_PATH is required}
|
|
connector=${GOCHAT_CONNECTOR_DB_PATH:?GOCHAT_CONNECTOR_DB_PATH is required}
|
|
passphrase_file=${GOCHAT_BACKUP_PASSPHRASE_FILE:?GOCHAT_BACKUP_PASSPHRASE_FILE is required}
|
|
started_at=$(date -u +%s)
|
|
|
|
[[ -f "$bundle" ]] || { echo "backup bundle does not exist: $bundle" >&2; exit 1; }
|
|
[[ -f "$bundle.sha256" ]] || { echo "backup checksum does not exist: $bundle.sha256" >&2; exit 1; }
|
|
(cd "$(dirname "$bundle")" && sha256sum -c "$(basename "$bundle").sha256")
|
|
|
|
server_major=$(($(psql "$dsn" -Atqc "SHOW server_version_num") / 10000))
|
|
client_major=$(pg_restore --version | awk '{print $NF}' | cut -d. -f1)
|
|
[[ "$client_major" == "$server_major" ]] || {
|
|
echo "pg_restore major $client_major must match PostgreSQL major $server_major" >&2
|
|
exit 1
|
|
}
|
|
|
|
table_count=$(psql "$dsn" -Atqc "SELECT count(*) FROM pg_tables WHERE schemaname = 'public'")
|
|
[[ "$table_count" == "0" ]] || { echo "target database is not empty" >&2; exit 1; }
|
|
install -d -m 0700 "$storage" "$(dirname "$connector")"
|
|
[[ -z "$(find "$storage" -mindepth 1 -maxdepth 1 -print -quit)" ]] || { echo "target storage is not empty" >&2; exit 1; }
|
|
[[ ! -e "$connector" ]] || { echo "target connector database already exists" >&2; exit 1; }
|
|
|
|
work_dir=$(mktemp -d "$(dirname "$bundle")/.restore.XXXXXX")
|
|
trap 'rm -rf "$work_dir"' EXIT
|
|
archive=$work_dir/snapshot.tar
|
|
openssl enc -d -aes-256-cbc -pbkdf2 -pass "file:$passphrase_file" -in "$bundle" -out "$archive"
|
|
while IFS= read -r path; do
|
|
case "$path" in
|
|
/*|../*|*/../*|*/..) echo "unsafe archive path: $path" >&2; exit 1 ;;
|
|
esac
|
|
done < <(tar -tf "$archive")
|
|
if tar -tvf "$archive" | awk '{type = substr($1, 1, 1); if (type != "-" && type != "d") found = 1} END {exit(found ? 0 : 1)}'; then
|
|
echo "backup archive contains unsupported entry types" >&2
|
|
exit 1
|
|
fi
|
|
tar -C "$work_dir" -xf "$archive"
|
|
rm "$archive"
|
|
(cd "$work_dir" && sha256sum -c SHA256SUMS)
|
|
pg_restore --list "$work_dir/postgres.dump" >/dev/null
|
|
|
|
pg_restore --exit-on-error --no-owner --no-privileges --dbname "$dsn" "$work_dir/postgres.dump"
|
|
while IFS= read -r path; do
|
|
case "$path" in
|
|
/*|../*|*/../*|*/..) echo "unsafe attachment path: $path" >&2; exit 1 ;;
|
|
esac
|
|
done < <(tar -tf "$work_dir/attachments.tar")
|
|
if tar -tvf "$work_dir/attachments.tar" | awk '{type = substr($1, 1, 1); if (type != "-" && type != "d") found = 1} END {exit(found ? 0 : 1)}'; then
|
|
echo "attachment archive contains unsupported entry types" >&2
|
|
exit 1
|
|
fi
|
|
tar -C "$storage" -xf "$work_dir/attachments.tar"
|
|
install -m 0600 "$work_dir/connector.db" "$connector"
|
|
|
|
migration_version=$(psql "$dsn" -Atqc "SELECT COALESCE(MAX(version), 0) FROM schema_migrations WHERE NOT dirty")
|
|
accounts=$(psql "$dsn" -Atqc "SELECT count(*) FROM accounts WHERE deleted_at IS NULL")
|
|
attachments=$(psql "$dsn" -Atqc "SELECT count(*) FROM attachments WHERE deleted_at IS NULL")
|
|
backup_epoch=$(awk -F= '$1 == "created_at_epoch" {print $2}' "$work_dir/manifest")
|
|
version=$(awk -F= '$1 == "version" {print substr($0, index($0, "=") + 1)}' "$work_dir/manifest")
|
|
finished_at=$(date -u +%s)
|
|
|
|
echo "restore=ok version=$version migration=$migration_version accounts=$accounts attachments=$attachments rpo_seconds=$((started_at-backup_epoch)) rto_seconds=$((finished_at-started_at))"
|