From 77bf794b73d403e9bbe550772990ea1ac623684e Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Thu, 26 Mar 2026 14:25:35 +0100 Subject: [PATCH] Harden deployment with data backups and restore script --- README.md | 14 +++++ scripts/.deploy.env.example | 6 ++ scripts/deploy.sh | 48 +++++++++++++++ scripts/restore-data.sh | 115 ++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 scripts/restore-data.sh diff --git a/README.md b/README.md index 18d54c6..7bfe56f 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,20 @@ Use the provided deploy script to avoid deleting runtime files in `data/`: This script deploys with `rsync --delete` but explicitly excludes `data/`. +Hardening built in: +- Creates a remote pre-deploy backup archive of `data/` +- Keeps the latest N backups (`DEPLOY_BACKUP_KEEP`, default `14`) +- Supports dry runs (`DEPLOY_DRY_RUN=1`) +- Configurable via `scripts/.deploy.env` + +Restore examples: + +```bash +./scripts/restore-data.sh --list +./scripts/restore-data.sh --latest +./scripts/restore-data.sh --file data-YYYYMMDD-HHMMSS.tgz +``` + --- ## Security diff --git a/scripts/.deploy.env.example b/scripts/.deploy.env.example index e42b826..63054a3 100644 --- a/scripts/.deploy.env.example +++ b/scripts/.deploy.env.example @@ -1,3 +1,9 @@ # Copy this file to scripts/.deploy.env and fill in your values. DEPLOY_HOST="root@example.com" DEPLOY_TARGET="/home/user/web/xmrpay.link/public_html" + +# Optional hardening settings: +# DEPLOY_BACKUP_ENABLE="1" +# DEPLOY_BACKUP_KEEP="14" +# DEPLOY_BACKUP_DIR="/home/user/web/xmrpay.link/backups/xmrpay-data" +# DEPLOY_DRY_RUN="0" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 35afe1c..e1bd22a 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -7,6 +7,12 @@ set -euo pipefail # DEPLOY_HOST e.g. root@example.com or deploy@example.com # DEPLOY_TARGET e.g. /home/user/web/xmrpay.link/public_html # +# Optional hardening: +# DEPLOY_BACKUP_ENABLE=1 # 1=backup data before deploy, 0=disable +# DEPLOY_BACKUP_KEEP=14 # number of backup archives to keep +# DEPLOY_BACKUP_DIR=... # remote backup folder +# DEPLOY_DRY_RUN=0 # 1=rsync dry-run +# # Optional local config file (not committed): # scripts/.deploy.env @@ -20,6 +26,10 @@ fi HOST="${DEPLOY_HOST:-}" TARGET="${DEPLOY_TARGET:-}" +BACKUP_ENABLE="${DEPLOY_BACKUP_ENABLE:-1}" +BACKUP_KEEP="${DEPLOY_BACKUP_KEEP:-14}" +BACKUP_DIR="${DEPLOY_BACKUP_DIR:-$TARGET/../backups/xmrpay-data}" +DRY_RUN="${DEPLOY_DRY_RUN:-0}" if [[ -z "$HOST" || -z "$TARGET" ]]; then echo "Missing deploy configuration." >&2 @@ -27,7 +37,45 @@ if [[ -z "$HOST" || -z "$TARGET" ]]; then exit 1 fi +if [[ "$BACKUP_ENABLE" == "1" ]]; then + echo "Creating remote pre-deploy data backup..." + ssh "$HOST" " + set -euo pipefail + TARGET='$TARGET' + DATA_DIR=\"\$TARGET/data\" + BACKUP_DIR='$BACKUP_DIR' + KEEP='$BACKUP_KEEP' + + mkdir -p \"\$BACKUP_DIR\" + if [ -d \"\$DATA_DIR\" ]; then + TS=\$(date +%Y%m%d-%H%M%S) + ARCHIVE=\"\$BACKUP_DIR/data-\$TS.tgz\" + tar -C \"\$TARGET\" -czf \"\$ARCHIVE\" data + echo \"backup_created=\$ARCHIVE\" + + COUNT=0 + for FILE in \$(ls -1t \"\$BACKUP_DIR\"/data-*.tgz 2>/dev/null || true); do + COUNT=\$((COUNT + 1)) + if [ \"\$COUNT\" -gt \"\$KEEP\" ]; then + rm -f \"\$FILE\" + fi + done + else + echo \"backup_skipped=no_data_dir\" + fi + " +else + echo "Skipping pre-deploy backup (DEPLOY_BACKUP_ENABLE=0)." +fi + +RSYNC_DRY_RUN=() +if [[ "$DRY_RUN" == "1" ]]; then + RSYNC_DRY_RUN+=("--dry-run") + echo "Running in dry-run mode (DEPLOY_DRY_RUN=1)." +fi + rsync -avz --delete \ + "${RSYNC_DRY_RUN[@]}" \ --exclude '.git' \ --exclude 'node_modules' \ --exclude 'data/' \ diff --git a/scripts/restore-data.sh b/scripts/restore-data.sh new file mode 100644 index 0000000..1a5b9c5 --- /dev/null +++ b/scripts/restore-data.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Restore runtime data/ from remote backup archives created by deploy.sh +# +# Usage: +# ./scripts/restore-data.sh --latest +# ./scripts/restore-data.sh --file data-20260326-120000.tgz +# ./scripts/restore-data.sh --list +# +# Config (env vars or scripts/.deploy.env): +# DEPLOY_HOST +# DEPLOY_TARGET +# DEPLOY_BACKUP_DIR (optional, defaults to ../backups/xmrpay-data) + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$SCRIPT_DIR/.deploy.env" + +if [[ -f "$ENV_FILE" ]]; then + # shellcheck disable=SC1090 + source "$ENV_FILE" +fi + +HOST="${DEPLOY_HOST:-}" +TARGET="${DEPLOY_TARGET:-}" +BACKUP_DIR="${DEPLOY_BACKUP_DIR:-$TARGET/../backups/xmrpay-data}" + +if [[ -z "$HOST" || -z "$TARGET" ]]; then + echo "Missing configuration." >&2 + echo "Set DEPLOY_HOST and DEPLOY_TARGET (env vars or scripts/.deploy.env)." >&2 + exit 1 +fi + +MODE="" +ARCHIVE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --latest) + MODE="latest" + shift + ;; + --file) + MODE="file" + ARCHIVE="${2:-}" + shift 2 + ;; + --list) + MODE="list" + shift + ;; + -h|--help) + MODE="help" + shift + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "$MODE" || "$MODE" == "help" ]]; then + sed -n '1,20p' "$0" + exit 0 +fi + +if [[ "$MODE" == "list" ]]; then + ssh "$HOST" "ls -1t '$BACKUP_DIR'/data-*.tgz 2>/dev/null || true" + exit 0 +fi + +if [[ "$MODE" == "latest" ]]; then + ARCHIVE="$(ssh "$HOST" "ls -1t '$BACKUP_DIR'/data-*.tgz 2>/dev/null | head -n 1")" + if [[ -z "$ARCHIVE" ]]; then + echo "No backup archives found in $BACKUP_DIR" >&2 + exit 1 + fi +fi + +if [[ "$MODE" == "file" ]]; then + if [[ -z "$ARCHIVE" ]]; then + echo "--file requires an archive name" >&2 + exit 1 + fi + ARCHIVE="$BACKUP_DIR/$ARCHIVE" +fi + +echo "Restoring data from: $ARCHIVE" +ssh "$HOST" " + set -euo pipefail + TARGET='$TARGET' + DATA_DIR=\"\$TARGET/data\" + ARCHIVE='$ARCHIVE' + + if [ ! -f \"\$ARCHIVE\" ]; then + echo 'Backup archive not found: '\"\$ARCHIVE\" >&2 + exit 1 + fi + + TS=\$(date +%Y%m%d-%H%M%S) + SAFETY=\"\$TARGET/../backups/xmrpay-data/pre-restore-\$TS.tgz\" + mkdir -p \"\$(dirname \"\$SAFETY\")\" + + if [ -d \"\$DATA_DIR\" ]; then + tar -C \"\$TARGET\" -czf \"\$SAFETY\" data + rm -rf \"\$DATA_DIR\" + fi + + tar -C \"\$TARGET\" -xzf \"\$ARCHIVE\" + echo \"restore_ok=\$ARCHIVE\" + echo \"safety_backup=\$SAFETY\" +" + +echo "Restore complete."