7faa9098de
Restructure around a single entry point (automations.sh) with a Gum wizard and a self-extracting bundle for repo-less installs. Add scripts/oslib.sh so the provisioning scripts (setup-host, harden-ssh, harden-jumphost, sshuser) run on Alpine/Debian/Alma; seed root keys from globals/. - ntfy SSH-login alerts (user, source IP, key, region, jump target) via pam_exec - daily auto-updates with AUTO_REBOOT=idle (reboots only when no SSH active) and opt-in Alpine stable-branch upgrades - generic + per-deployment cloud-init; Gitea release workflow on tag - README/LICENSE/.gitignore/.gitattributes (force LF); repo URLs -> Gitea
417 lines
14 KiB
Bash
417 lines
14 KiB
Bash
#!/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 <backup-file.tar.gz.age>"
|
|
[[ -f "$BACKUP_FILE" ]] || die "Backup file '$BACKUP_FILE' not found."
|
|
|
|
if [[ "$SKIP_VALIDATION" != "1" ]]; then
|
|
cat <<EOF
|
|
WARNING: This will restore a SimpleX server backup to this host.
|
|
|
|
This script will:
|
|
1. Install packages (docker, openssh, awall, sshguard, age)
|
|
2. Restore SSH hardening (may change your SSH config)
|
|
3. Restore firewall rules (may change your network access)
|
|
4. Deploy the SimpleX docker stack
|
|
5. Restore all server keys and certificates
|
|
|
|
ONLY run this on a fresh Alpine installation intended to become
|
|
a SimpleX relay server. This will overwrite existing configs.
|
|
|
|
Continue? [y/N]
|
|
EOF
|
|
read -r ans
|
|
[[ "${ans,,}" =~ ^(y|yes)$ ]] || die "Aborted."
|
|
fi
|
|
|
|
log "Starting restore from: $(basename "$BACKUP_FILE")"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# 2. Install age and decrypt backup
|
|
# ----------------------------------------------------------------------------
|
|
if ! command -v age >/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 <<EOF
|
|
|
|
================================================================
|
|
RESTORE COMPLETE
|
|
================================================================
|
|
|
|
Services Status:
|
|
$(rc-service --list | grep -E '(docker|sshd|iptables|sshguard)' || true)
|
|
|
|
Next Steps:
|
|
|
|
1. VERIFY SSH ACCESS STILL WORKS
|
|
Test from another terminal before closing this session:
|
|
ssh -i ~/.ssh/your_key root@$(hostname)
|
|
|
|
2. CHECK SIMPLEX SERVICES
|
|
cd $SIMPLEX_DIR && docker compose ps
|
|
./print-addresses.sh
|
|
|
|
3. VERIFY SERVER FINGERPRINTS
|
|
Compare these to your original backup to confirm identities were preserved:
|
|
|
|
SSH host key:
|
|
$(ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub 2>/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
|