Harden deployment with data backups and restore script
This commit is contained in:
14
README.md
14
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/`.
|
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
|
## Security
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
# Copy this file to scripts/.deploy.env and fill in your values.
|
# Copy this file to scripts/.deploy.env and fill in your values.
|
||||||
DEPLOY_HOST="root@example.com"
|
DEPLOY_HOST="root@example.com"
|
||||||
DEPLOY_TARGET="/home/user/web/xmrpay.link/public_html"
|
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"
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ set -euo pipefail
|
|||||||
# DEPLOY_HOST e.g. root@example.com or deploy@example.com
|
# DEPLOY_HOST e.g. root@example.com or deploy@example.com
|
||||||
# DEPLOY_TARGET e.g. /home/user/web/xmrpay.link/public_html
|
# 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):
|
# Optional local config file (not committed):
|
||||||
# scripts/.deploy.env
|
# scripts/.deploy.env
|
||||||
|
|
||||||
@@ -20,6 +26,10 @@ fi
|
|||||||
|
|
||||||
HOST="${DEPLOY_HOST:-}"
|
HOST="${DEPLOY_HOST:-}"
|
||||||
TARGET="${DEPLOY_TARGET:-}"
|
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
|
if [[ -z "$HOST" || -z "$TARGET" ]]; then
|
||||||
echo "Missing deploy configuration." >&2
|
echo "Missing deploy configuration." >&2
|
||||||
@@ -27,7 +37,45 @@ if [[ -z "$HOST" || -z "$TARGET" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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 -avz --delete \
|
||||||
|
"${RSYNC_DRY_RUN[@]}" \
|
||||||
--exclude '.git' \
|
--exclude '.git' \
|
||||||
--exclude 'node_modules' \
|
--exclude 'node_modules' \
|
||||||
--exclude 'data/' \
|
--exclude 'data/' \
|
||||||
|
|||||||
115
scripts/restore-data.sh
Normal file
115
scripts/restore-data.sh
Normal file
@@ -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."
|
||||||
Reference in New Issue
Block a user