#!/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 script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) "$script_dir/database_client_entrypoint.sh" --check 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} metrics_file=${GOCHAT_BACKUP_METRICS_FILE:-} 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 if [[ -n $metrics_file ]]; then install -d -m 0755 "$(dirname "$metrics_file")" metrics_tmp=$metrics_file.tmp { echo '# HELP gochat_backup_last_success_timestamp_seconds Unix time of the last verified off-site backup.' echo '# TYPE gochat_backup_last_success_timestamp_seconds gauge' echo "gochat_backup_last_success_timestamp_seconds $created_at_epoch" echo '# HELP gochat_backup_rpo_target_seconds Maximum allowed age of the latest backup.' echo '# TYPE gochat_backup_rpo_target_seconds gauge' echo 'gochat_backup_rpo_target_seconds 86400' } >"$metrics_tmp" chmod 0644 "$metrics_tmp" mv "$metrics_tmp" "$metrics_file" fi echo "backup=$bundle offsite=$offsite_dir/$(basename "$bundle") version=$version created_at=$timestamp"