76 lines
3.8 KiB
Bash
76 lines
3.8 KiB
Bash
#!/usr/bin/env bash
|
|
# Run on the backup PC (Git Bash), after the server has made encrypted backups.
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
: "${PILOT_SSH_TARGET:?Set the configured SSH alias for the pilot server}"
|
|
: "${PILOT_REMOTE_BACKUP_DIRECTORY:?Set its absolute encrypted backup directory}"
|
|
: "${BACKUP_LOCAL_DIRECTORY:?Set an absolute backup directory on this PC}"
|
|
|
|
[[ "$PILOT_SSH_TARGET" =~ ^[a-zA-Z0-9_.@-]+$ ]] || { echo 'Invalid SSH target' >&2; exit 1; }
|
|
[[ "$PILOT_REMOTE_BACKUP_DIRECTORY" =~ ^/[a-zA-Z0-9_./-]+$ ]] || { echo 'Invalid server backup directory' >&2; exit 1; }
|
|
[[ "$PILOT_REMOTE_BACKUP_DIRECTORY" != */../* && "$PILOT_REMOTE_BACKUP_DIRECTORY" != */.. ]] || { echo 'Parent traversal is not allowed' >&2; exit 1; }
|
|
[[ "$PILOT_REMOTE_BACKUP_DIRECTORY" != / ]] || { echo 'Use a dedicated server backup directory' >&2; exit 1; }
|
|
[[ "$BACKUP_LOCAL_DIRECTORY" = /* && "$BACKUP_LOCAL_DIRECTORY" != / ]] || { echo 'Use a dedicated absolute local directory' >&2; exit 1; }
|
|
[[ "$BACKUP_LOCAL_DIRECTORY" != */../* && "$BACKUP_LOCAL_DIRECTORY" != */.. ]] || { echo 'Parent traversal is not allowed' >&2; exit 1; }
|
|
[[ ! -L "$BACKUP_LOCAL_DIRECTORY" ]] || { echo 'Local directory must not be a symlink' >&2; exit 1; }
|
|
|
|
mkdir -p -- "$BACKUP_LOCAL_DIRECTORY"
|
|
local_dir=$(cd -- "$BACKUP_LOCAL_DIRECTORY" && pwd -P)
|
|
[[ "$local_dir" != / && "$local_dir" != /tmp && "$local_dir" != /var && "$local_dir" != /home ]] || {
|
|
echo 'Local backup path resolves to a broad directory' >&2
|
|
exit 1
|
|
}
|
|
cd -- "$local_dir"
|
|
|
|
remote_dir=${PILOT_REMOTE_BACKUP_DIRECTORY%/}
|
|
listing=$(ssh -o BatchMode=yes -- "$PILOT_SSH_TARGET" \
|
|
"find '$remote_dir' -maxdepth 1 -type f -name 'krona-staff-*.dump.cms' -printf '%f\n'")
|
|
[[ -n "$listing" ]] || { echo 'No encrypted backups found on server' >&2; exit 1; }
|
|
|
|
copied=0
|
|
latest_backup_epoch=0
|
|
while IFS= read -r name; do
|
|
[[ "$name" =~ ^krona-staff-[0-9]{8}T[0-9]{6}Z-[0-9]+\.dump\.cms$ ]] || {
|
|
echo 'Unexpected backup filename from server' >&2
|
|
exit 1
|
|
}
|
|
timestamp=${name#krona-staff-}
|
|
timestamp=${timestamp%%-*}
|
|
backup_epoch=$(date -u -d "${timestamp:0:4}-${timestamp:4:2}-${timestamp:6:2} ${timestamp:9:2}:${timestamp:11:2}:${timestamp:13:2} UTC" +%s)
|
|
if (( backup_epoch > latest_backup_epoch )); then
|
|
latest_backup_epoch=$backup_epoch
|
|
fi
|
|
|
|
if [[ -e "$name" || -e "$name.sha256" ]]; then
|
|
[[ -f "$name" && -f "$name.sha256" ]] || { echo "Incomplete local copy: $name" >&2; exit 1; }
|
|
sha256sum -c -- "$name.sha256" >/dev/null || { echo "Local checksum mismatch: $name" >&2; exit 1; }
|
|
continue
|
|
fi
|
|
|
|
encrypted_partial="$name.partial"
|
|
checksum_partial="$name.sha256.partial"
|
|
[[ ! -e "$encrypted_partial" && ! -e "$checksum_partial" ]] || {
|
|
echo "Interrupted prior copy needs inspection: $name" >&2
|
|
exit 1
|
|
}
|
|
scp -B -- "$PILOT_SSH_TARGET:$remote_dir/$name" "$encrypted_partial"
|
|
scp -B -- "$PILOT_SSH_TARGET:$remote_dir/$name.sha256" "$checksum_partial"
|
|
expected=$(awk 'NR == 1 { print $1 }' "$checksum_partial")
|
|
[[ "$expected" =~ ^[[:xdigit:]]{64}$ ]] || { echo "Malformed checksum: $name" >&2; exit 1; }
|
|
actual=$(sha256sum -- "$encrypted_partial")
|
|
[[ "${actual%% *}" = "$expected" ]] || { echo "Transfer checksum mismatch: $name" >&2; exit 1; }
|
|
mv -- "$encrypted_partial" "$name"
|
|
mv -- "$checksum_partial" "$name.sha256"
|
|
copied=$((copied + 1))
|
|
done <<< "$listing"
|
|
|
|
max_age_hours=${BACKUP_MAX_AGE_HOURS:-36}
|
|
[[ "$max_age_hours" =~ ^[1-9][0-9]*$ ]] || { echo 'BACKUP_MAX_AGE_HOURS must be a positive integer' >&2; exit 1; }
|
|
now_epoch=$(date -u +%s)
|
|
(( now_epoch >= latest_backup_epoch && now_epoch - latest_backup_epoch <= max_age_hours * 3600 )) || {
|
|
echo 'Newest encrypted backup is stale or has a future timestamp' >&2
|
|
exit 1
|
|
}
|
|
|
|
printf 'off_host_backup_count=%s\nnewly_copied=%s\n' "$(find . -maxdepth 1 -type f -name 'krona-staff-*.dump.cms' | wc -l)" "$copied"
|