* HH-441: harden durable storage and recovery * HH-441: clear recovery review blockers * HH-441: enforce offsite backup failure domain --------- Co-authored-by: Rogee <rogee@ipao.vip>
63 lines
2.8 KiB
Bash
Executable File
63 lines
2.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Create one encrypted, off-site copy containing PostgreSQL, attachments, and
|
|
# the Connector's online SQLite backup.
|
|
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
dsn=${GOCHAT_DATABASE_DSN:?GOCHAT_DATABASE_DSN is required}
|
|
storage=${GOCHAT_STORAGE_PATH:?GOCHAT_STORAGE_PATH is required}
|
|
connector=${GOCHAT_CONNECTOR_BACKUP_FILE:?GOCHAT_CONNECTOR_BACKUP_FILE is required}
|
|
backup_dir=${GOCHAT_BACKUP_DIR:-/var/backups/gochat}
|
|
offsite_dir=${GOCHAT_BACKUP_OFFSITE_DIR:?GOCHAT_BACKUP_OFFSITE_DIR is required}
|
|
passphrase_file=${GOCHAT_BACKUP_PASSPHRASE_FILE:?GOCHAT_BACKUP_PASSPHRASE_FILE is required}
|
|
retention_days=${GOCHAT_BACKUP_RETENTION_DAYS:-30}
|
|
version=${GOCHAT_VERSION:-unknown}
|
|
timestamp=$(date -u +%Y%m%dT%H%M%SZ)
|
|
|
|
[[ -d "$storage" ]] || { echo "storage path does not exist: $storage" >&2; exit 1; }
|
|
[[ -z "$(find "$storage" -type l -print -quit)" ]] || { echo "storage must not contain symbolic links" >&2; exit 1; }
|
|
[[ -f "$connector" ]] || { echo "connector backup does not exist: $connector" >&2; exit 1; }
|
|
[[ -r "$passphrase_file" ]] || { echo "backup passphrase file is not readable" >&2; exit 1; }
|
|
server_major=$(($(psql "$dsn" -Atqc "SHOW server_version_num") / 10000))
|
|
client_major=$(pg_dump --version | awk '{print $NF}' | cut -d. -f1)
|
|
[[ "$client_major" == "$server_major" ]] || {
|
|
echo "pg_dump major $client_major must match PostgreSQL major $server_major" >&2
|
|
exit 1
|
|
}
|
|
|
|
install -d -m 0700 "$backup_dir" "$offsite_dir"
|
|
[[ "$(realpath "$backup_dir")" != "$(realpath "$offsite_dir")" ]] || {
|
|
echo "off-site directory must use a different path/failure domain" >&2
|
|
exit 1
|
|
}
|
|
|
|
work_dir=$(mktemp -d "$backup_dir/.backup.XXXXXX")
|
|
trap 'rm -rf "$work_dir"' EXIT
|
|
stage=$work_dir/snapshot
|
|
mkdir "$stage"
|
|
|
|
pg_dump --dbname "$dsn" --format=custom --compress=9 --file "$stage/postgres.dump"
|
|
pg_restore --list "$stage/postgres.dump" >/dev/null
|
|
tar -C "$storage" -cf "$stage/attachments.tar" .
|
|
cp "$connector" "$stage/connector.db"
|
|
|
|
created_at_epoch=$(date -u +%s)
|
|
{
|
|
echo "created_at=$timestamp"
|
|
echo "created_at_epoch=$created_at_epoch"
|
|
echo "version=$version"
|
|
echo "rpo_target_seconds=86400"
|
|
} >"$stage/manifest"
|
|
(cd "$stage" && sha256sum postgres.dump attachments.tar connector.db manifest >SHA256SUMS)
|
|
|
|
bundle=$backup_dir/gochat-$timestamp.tar.enc
|
|
tar -C "$stage" -cf - . | openssl enc -aes-256-cbc -pbkdf2 -salt -pass "file:$passphrase_file" -out "$bundle"
|
|
(cd "$backup_dir" && sha256sum "$(basename "$bundle")" >"$(basename "$bundle").sha256")
|
|
openssl enc -d -aes-256-cbc -pbkdf2 -pass "file:$passphrase_file" -in "$bundle" | tar -tf - >/dev/null
|
|
|
|
cp "$bundle" "$bundle.sha256" "$offsite_dir/"
|
|
find "$backup_dir" "$offsite_dir" -maxdepth 1 -type f -name 'gochat-*.tar.enc*' -mtime "+$retention_days" -delete
|
|
|
|
echo "backup=$bundle offsite=$offsite_dir/$(basename "$bundle") version=$version created_at=$timestamp"
|