Files
story/deploy/server/verify-backup.sh
T

107 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
project_root=${PLATFORM_ROOT:-/opt/data-terminal-cloud/current}
backup_root=${BACKUP_ROOT:-/var/backups/data-terminal-cloud}
passphrase_file=${BACKUP_PASSPHRASE_FILE:-/etc/data-terminal-cloud/backup-passphrase}
archive=${1:-}
if [[ -z "$archive" ]]; then
archive=$(find "$backup_root" -maxdepth 1 -type f -name 'platform-*.tar.gz.enc' -printf '%T@ %p\n' | sort -nr | head -n1 | cut -d' ' -f2-)
fi
if [[ -z "$archive" || ! -f "$archive" ]]; then
echo "Backup archive was not found" >&2
exit 2
fi
archive=$(realpath "$archive")
backup_root=$(realpath "$backup_root")
case "$archive" in
"$backup_root"/platform-*.tar.gz.enc) ;;
*) echo "Backup archive is outside the configured backup directory" >&2; exit 2 ;;
esac
if [[ ! -r "$passphrase_file" ]]; then
echo "Backup passphrase file is missing or unreadable" >&2
exit 2
fi
compose=(docker compose -f "$project_root/compose.yaml" -f "$project_root/deploy/server/compose.override.yaml" --env-file "$project_root/deploy/server/.env")
postgres_container=$("${compose[@]}" ps -q postgres)
if [[ -z "$postgres_container" ]] || [[ $(docker inspect -f '{{.State.Running}}' "$postgres_container") != true ]]; then
echo "PostgreSQL service is not running" >&2
exit 3
fi
redis_container=$("${compose[@]}" ps -q redis)
if [[ -z "$redis_container" ]] || [[ $(docker inspect -f '{{.State.Running}}' "$redis_container") != true ]]; then
echo "Redis service is not running" >&2
exit 3
fi
umask 077
work=$(mktemp -d "$backup_root/.verify-XXXXXX")
plain="$work/backup.tar.gz"
verify_token=$(openssl rand -hex 12)
restore_database="voice_restore_verify_$verify_token"
redis_verify_path="/tmp/platform-backup-$verify_token.rdb"
database_created=false
cleanup() {
status=$?
if [[ "$database_created" == true ]]; then
docker exec "$postgres_container" dropdb -U voice --if-exists "$restore_database" >/dev/null 2>&1 || true
fi
docker exec "$redis_container" rm -f -- "$redis_verify_path" >/dev/null 2>&1 || true
rm -rf -- "$work"
exit "$status"
}
trap cleanup EXIT INT TERM
if [[ -f "$archive.sha256" ]]; then
(cd "$(dirname "$archive")" && sha256sum -c "$(basename "$archive").sha256") >/dev/null
fi
openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
-pass "file:$passphrase_file" -in "$archive" -out "$plain"
tar -tzf "$plain" >"$work/archive-paths.txt"
if grep -Eq '(^/|(^|/)\.\.(/|$))' "$work/archive-paths.txt"; then
echo "Backup contains an unsafe archive path" >&2
exit 4
fi
mkdir -p "$work/extracted"
tar -xzf "$plain" --no-same-owner -C "$work/extracted"
(
cd "$work/extracted"
sha256sum -c SHA256SUMS >/dev/null
)
expected=$(tr -d '\r\n' <"$work/extracted/database-evidence.txt")
docker exec "$postgres_container" createdb -U voice "$restore_database"
database_created=true
docker exec -i "$postgres_container" pg_restore -U voice -d "$restore_database" --no-owner --no-acl <"$work/extracted/postgres.dump"
actual=$(docker exec "$postgres_container" psql -U voice -d "$restore_database" -Atc \
"SELECT version||'|'||dirty||'|'||(SELECT count(*) FROM tenant)||'|'||(SELECT count(*) FROM user_account)||'|'||(SELECT count(*) FROM information_schema.tables WHERE table_schema='public')||'|'||(SELECT count(*) FROM information_schema.table_constraints WHERE constraint_type='FOREIGN KEY') FROM schema_migrations;")
if [[ "$actual" != "$expected" ]]; then
echo "Restored database evidence does not match the backup: expected=$expected actual=$actual" >&2
exit 5
fi
[[ -s "$work/extracted/redis/dump.rdb" ]] || { echo "Redis snapshot is missing" >&2; exit 6; }
[[ -d "$work/extracted/object-store" ]] || { echo "Object-store snapshot is missing" >&2; exit 6; }
[[ -d "$work/extracted/keycloak" ]] || { echo "Keycloak snapshot is missing" >&2; exit 6; }
expected_object_files=$(sed -n 's/^object_files=//p' "$work/extracted/manifest.properties")
expected_keycloak_files=$(sed -n 's/^keycloak_files=//p' "$work/extracted/manifest.properties")
actual_object_files=$(find "$work/extracted/object-store" -type f | wc -l | tr -d ' ')
actual_keycloak_files=$(find "$work/extracted/keycloak" -type f | wc -l | tr -d ' ')
if [[ ! "$expected_object_files" =~ ^[0-9]+$ || "$actual_object_files" != "$expected_object_files" ]]; then
echo "Object-store file count does not match the manifest" >&2
exit 6
fi
if [[ ! "$expected_keycloak_files" =~ ^[1-9][0-9]*$ || "$actual_keycloak_files" != "$expected_keycloak_files" ]]; then
echo "Keycloak file count does not match the manifest" >&2
exit 6
fi
docker cp "$work/extracted/redis/dump.rdb" "$redis_container:$redis_verify_path" >/dev/null
docker exec "$redis_container" redis-check-rdb "$redis_verify_path" >/dev/null
docker exec "$redis_container" rm -f -- "$redis_verify_path"
printf 'backup_restore_verified archive=%s database_evidence=%s object_files=%s keycloak_files=%s redis_rdb=valid\n' \
"$archive" "$actual" "$actual_object_files" "$actual_keycloak_files"