Files
57_Wolve 7faa9098de feat: unified launcher, multi-OS hardening, login alerts & auto-updates
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
2026-06-12 14:56:02 -05:00

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