#!/usr/bin/env bash # # restore.sh # # Restores a SimpleX server from an age-encrypted backup created by backup.sh. # This script can rebuild the entire server setup from scratch while preserving: # - Server fingerprints (SimpleX CA keys) # - .onion addresses (Tor hidden service keys) # - SSH host key fingerprint # - SSH authorized_keys and hardened config # - All server configurations # # IMPORTANT: Run this on a FRESH Alpine installation. This script will: # 1. Decrypt and extract the backup # 2. Install all required packages # 3. Restore SSH hardening + keys # 4. Restore firewall config # 5. Deploy the SimpleX stack with preserved keys # 6. Start services # # Usage: # # Copy restore.sh and your backup onto fresh Alpine host # scp restore.sh backup-YYYYMMDD-HHMMSS.tar.gz.age root@new-host:/root/ # # # Run restore (will prompt for age private key) # ssh root@new-host # bash restore.sh backup-YYYYMMDD-HHMMSS.tar.gz.age # # # Alternative: pass age identity file # AGE_IDENTITY=/root/backup-key.txt bash restore.sh backup.tar.gz.age set -euo pipefail # ============================================================================ # CONFIG # ============================================================================ : "${AGE_IDENTITY:=}" # optional: path to age private key file : "${FORCE_OVERWRITE:=0}" # set to 1 to overwrite existing files : "${SKIP_VALIDATION:=0}" # set to 1 to skip "are you sure" prompts BACKUP_FILE="${1:-}" log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; } # ---------------------------------------------------------------------------- # 1. Validation and setup # ---------------------------------------------------------------------------- [[ $EUID -eq 0 ]] || die "Run as root." [[ -f /etc/alpine-release ]] || die "This script targets Alpine Linux." [[ -n "$BACKUP_FILE" ]] || die "Usage: $0 " [[ -f "$BACKUP_FILE" ]] || die "Backup file '$BACKUP_FILE' not found." if [[ "$SKIP_VALIDATION" != "1" ]]; then cat </dev/null; then log "Installing age..." apk add -q age fi RESTORE_DIR="/tmp/restore-$$" mkdir -p "$RESTORE_DIR" cd "$RESTORE_DIR" log "Decrypting backup..." if [[ -n "$AGE_IDENTITY" ]]; then if [[ ! -f "$AGE_IDENTITY" ]]; then die "AGE_IDENTITY file '$AGE_IDENTITY' not found." fi age --decrypt --identity "$AGE_IDENTITY" "$BACKUP_FILE" | tar xzf - else # Interactive decryption - age will prompt for passphrase or identity echo "Enter your age private key or passphrase when prompted:" age --decrypt "$BACKUP_FILE" | tar xzf - fi # Find the extracted directory (should be simplex-backup-YYYYMMDD-HHMMSS) BACKUP_EXTRACT_DIR=$(find . -maxdepth 1 -type d -name 'simplex-backup-*' | head -1) [[ -n "$BACKUP_EXTRACT_DIR" ]] || die "Could not find extracted backup directory." BACKUP_EXTRACT_DIR=$(realpath "$BACKUP_EXTRACT_DIR") log "Extracted to: $BACKUP_EXTRACT_DIR" # ---------------------------------------------------------------------------- # 3. Install required packages # ---------------------------------------------------------------------------- log "Updating package lists and installing requirements..." # Enable community repo if not already ALPINE_VER=$(cut -d. -f1,2 < /etc/alpine-release) if ! grep -qE "^https?://.+/v${ALPINE_VER}/community" /etc/apk/repositories; then sed -i -E "s|^#(https?://.+/v${ALPINE_VER}/community)|\1|" /etc/apk/repositories || { MAIN_MIRROR=$(awk '/main$/ {print; exit}' /etc/apk/repositories \ | sed -E "s|/v${ALPINE_VER}/main|/v${ALPINE_VER}/community|") [[ -n "$MAIN_MIRROR" ]] && echo "$MAIN_MIRROR" >> /etc/apk/repositories } fi apk update -q apk upgrade -q apk add -q \ docker docker-cli-compose \ openssh openssh-server \ awall iptables ip6tables \ sshguard \ curl bash openssl \ ca-certificates openrc # ---------------------------------------------------------------------------- # 4. Restore SSH configuration and keys # ---------------------------------------------------------------------------- log "Restoring SSH configuration and keys..." # Back up existing SSH config if it exists [[ -f /etc/ssh/sshd_config ]] && cp /etc/ssh/sshd_config /etc/ssh/sshd_config.pre-restore # Restore SSH host key if [[ -f "$BACKUP_EXTRACT_DIR/etc/ssh/ssh_host_ed25519_key" ]]; then cp "$BACKUP_EXTRACT_DIR/etc/ssh/ssh_host_ed25519_key" /etc/ssh/ cp "$BACKUP_EXTRACT_DIR/etc/ssh/ssh_host_ed25519_key.pub" /etc/ssh/ chmod 600 /etc/ssh/ssh_host_ed25519_key chmod 644 /etc/ssh/ssh_host_ed25519_key.pub # Remove any other host key types rm -f /etc/ssh/ssh_host_rsa_key* /etc/ssh/ssh_host_ecdsa_key* /etc/ssh/ssh_host_dsa_key* log "Restored SSH host key: $(ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub)" else warn "No SSH host key found in backup; keeping system default" fi # Restore sshd_config if [[ -f "$BACKUP_EXTRACT_DIR/etc/ssh/sshd_config" ]]; then cp "$BACKUP_EXTRACT_DIR/etc/ssh/sshd_config" /etc/ssh/ log "Restored SSH daemon configuration" else warn "No sshd_config found in backup" fi # Restore root's authorized_keys mkdir -p /root/.ssh chmod 700 /root/.ssh if [[ -f "$BACKUP_EXTRACT_DIR/root/.ssh/authorized_keys" ]]; then cp "$BACKUP_EXTRACT_DIR/root/.ssh/authorized_keys" /root/.ssh/ chmod 600 /root/.ssh/authorized_keys KEY_COUNT=$(wc -l < /root/.ssh/authorized_keys) log "Restored ${KEY_COUNT} authorized SSH key(s) for root" else warn "No authorized_keys found in backup" fi # Validate SSH config if [[ -f /etc/ssh/sshd_config ]]; then if sshd -t 2>/dev/null; then log "SSH configuration validated" else warn "SSH configuration validation failed; keeping anyway" fi fi # ---------------------------------------------------------------------------- # 5. Restore firewall configuration # ---------------------------------------------------------------------------- log "Restoring firewall configuration..." # Load iptables kernel modules modprobe -q ip_tables || true modprobe -q ip6_tables || true modprobe -q iptable_nat || true modprobe -q iptable_filter || true # Restore awall policies if they exist if [[ -d "$BACKUP_EXTRACT_DIR/etc/awall/optional" ]]; then mkdir -p /etc/awall/optional cp -r "$BACKUP_EXTRACT_DIR/etc/awall/optional/"* /etc/awall/optional/ # Enable the restored policies cd /etc/awall/optional for policy in *.json; do [[ -f "$policy" ]] || continue POLICY_NAME=$(basename "$policy" .json) awall enable "$POLICY_NAME" log "Enabled awall policy: $POLICY_NAME" done # Activate firewall awall translate --output /etc/iptables 2>/dev/null || true awall translate --output /etc/ip6tables 2>/dev/null || true awall activate --force log "Firewall rules activated" else warn "No awall policies found in backup" fi # Enable services rc-update add iptables default || true rc-update add ip6tables default || true rc-update add sshd default || true rc-update add sshguard default || true rc-update add docker default || true # ---------------------------------------------------------------------------- # 6. Restore SimpleX configuration and keys # ---------------------------------------------------------------------------- SIMPLEX_DIR="/opt/simplex" log "Restoring SimpleX configuration to $SIMPLEX_DIR..." mkdir -p "$SIMPLEX_DIR" # Restore the core SimpleX files SIMPLEX_FILES=( ".env" "docker-compose.yml" "print-addresses.sh" "tor_conf/" ) for item in "${SIMPLEX_FILES[@]}"; do SRC="$BACKUP_EXTRACT_DIR/opt/simplex/$item" if [[ -e "$SRC" ]]; then cp -r "$SRC" "$SIMPLEX_DIR/" log "Restored: $item" else warn "Missing from backup: $item" fi done # Make script executable [[ -f "$SIMPLEX_DIR/print-addresses.sh" ]] && chmod +x "$SIMPLEX_DIR/print-addresses.sh" # Restore SimpleX server keys and configs mkdir -p "$SIMPLEX_DIR/smp_configs" "$SIMPLEX_DIR/xftp_configs" \ "$SIMPLEX_DIR/smp_state" "$SIMPLEX_DIR/xftp_state" \ "$SIMPLEX_DIR/xftp_files" "$SIMPLEX_DIR/smp_tor" "$SIMPLEX_DIR/xftp_tor" # SMP server restoration for key in ca.key server.crt server.key smp-server.ini; do SRC="$BACKUP_EXTRACT_DIR/opt/simplex/smp_configs/$key" if [[ -f "$SRC" ]]; then cp "$SRC" "$SIMPLEX_DIR/smp_configs/" log "Restored SMP: $key" fi done # XFTP server restoration for key in ca.key server.crt server.key file-server.ini; do SRC="$BACKUP_EXTRACT_DIR/opt/simplex/xftp_configs/$key" if [[ -f "$SRC" ]]; then cp "$SRC" "$SIMPLEX_DIR/xftp_configs/" log "Restored XFTP: $key" fi done # Tor hidden service keys for service in smp xftp; do for key in hs_ed25519_secret_key hs_ed25519_public_key hostname; do SRC="$BACKUP_EXTRACT_DIR/opt/simplex/${service}_tor/$key" if [[ -f "$SRC" ]]; then cp "$SRC" "$SIMPLEX_DIR/${service}_tor/" log "Restored Tor ($service): $key" fi done done # Set proper ownership and permissions chmod 600 "$SIMPLEX_DIR"/*_configs/ca.key 2>/dev/null || true chmod 600 "$SIMPLEX_DIR"/*_tor/hs_ed25519_secret_key 2>/dev/null || true # ---------------------------------------------------------------------------- # 7. Start services # ---------------------------------------------------------------------------- log "Starting services..." # Start firewall services rc-service iptables start || rc-service iptables restart || true rc-service ip6tables start || rc-service ip6tables restart || true # Configure and start sshguard (basic setup - backup.sh doesn't save the full config) mkdir -p /etc/sshguard cat > /etc/sshguard/sshguard.conf <<'EOF' BACKEND="/usr/libexec/sshg-fw-iptables" LOGREADER="LANG=C journalctl -afb -p info -n1 -u sshd -o cat" THRESHOLD=30 BLOCK_TIME=300 DETECTION_TIME=1800 PID_FILE=/run/sshguard.pid EOF # Basic sshguard iptables setup cat > /etc/local.d/sshguard-iptables.start <<'EOF' #!/bin/sh SSH_PORT=$(awk '/^Port / {print $2; exit}' /etc/ssh/sshd_config) SSH_PORT=${SSH_PORT:-22} for ipt in iptables ip6tables; do $ipt -N sshguard 2>/dev/null || true $ipt -C INPUT -p tcp --dport "$SSH_PORT" -j sshguard 2>/dev/null \ || $ipt -I INPUT -p tcp --dport "$SSH_PORT" -j sshguard done EOF chmod +x /etc/local.d/sshguard-iptables.start rc-update add local default 2>/dev/null || true /etc/local.d/sshguard-iptables.start rc-service sshguard start || true # Start docker rc-service docker start || true # Wait for docker socket for _ in $(seq 1 20); do [[ -S /var/run/docker.sock ]] && break sleep 1 done if [[ ! -S /var/run/docker.sock ]]; then die "Docker socket didn't appear after starting service" fi # ---------------------------------------------------------------------------- # 8. Deploy SimpleX stack # ---------------------------------------------------------------------------- if [[ -f "$SIMPLEX_DIR/docker-compose.yml" && -f "$SIMPLEX_DIR/.env" ]]; then log "Starting SimpleX docker stack..." cd "$SIMPLEX_DIR" docker compose pull docker compose up -d log "Waiting for services to stabilize..." sleep 10 # Show status docker compose ps else warn "SimpleX compose files not found; skipping docker deployment" fi # ---------------------------------------------------------------------------- # 9. SSH service restart (do this last so we don't kill our session early) # ---------------------------------------------------------------------------- log "Reloading SSH service with restored configuration..." rc-service sshd reload || rc-service sshd restart || true # ---------------------------------------------------------------------------- # 10. Cleanup and final report # ---------------------------------------------------------------------------- log "Cleaning up temporary files..." rm -rf "$RESTORE_DIR" # Display current status and next steps cat </dev/null | sed 's/^/ /' || echo " [not found]") SMP server (when ready): $(cd "$SIMPLEX_DIR" 2>/dev/null && docker compose logs smp-server 2>/dev/null | grep -m1 'Server address:' | sed 's/^/ /' || echo " [check with: cd $SIMPLEX_DIR && docker compose logs smp-server]") 4. VERIFY .ONION ADDRESSES These should match your backup: SMP: $(cat "$SIMPLEX_DIR/smp_tor/hostname" 2>/dev/null || echo "[not yet available]") XFTP: $(cat "$SIMPLEX_DIR/xftp_tor/hostname" 2>/dev/null || echo "[not yet available]") 5. UPDATE DNS Ensure smp.yourdomain.com and xftp.yourdomain.com point to this host 6. TEST CONNECTIONS Verify existing SimpleX contacts can still reach your server ================================================================ EOF