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

226 lines
8.0 KiB
Bash

#!/usr/bin/env bash
#
# backup.sh
#
# Creates an age-encrypted backup of all irreplaceable SimpleX server keys,
# state, and SSH configuration. This is the minimum data set you'd need to
# recreate the server without changing server fingerprints or .onion addresses.
#
# The backup is encrypted using the age public key you specify. Decrypt on
# another machine with:
# age --decrypt --identity /path/to/private_key backup-YYYYMMDD-HHMMSS.tar.gz.age | tar xzf -
#
# Usage:
# AGE_RECIPIENT=age1ql3z7hjy54...your-pubkey bash backup.sh
# AGE_RECIPIENT_FILE=/path/to/pubkeys.txt bash backup.sh
#
# You can specify either a single AGE_RECIPIENT public key, or an
# AGE_RECIPIENT_FILE containing one or more public keys (one per line).
# The backup will be encrypted to all provided keys.
set -euo pipefail
# ============================================================================
# CONFIG
# ============================================================================
: "${AGE_RECIPIENT:=}" # single age public key (age1...)
: "${AGE_RECIPIENT_FILE:=}" # file containing age public keys
: "${SIMPLEX_DIR:=/opt/simplex}"
: "${BACKUP_DIR:=/tmp}"
: "${KEEP_BACKUPS:=7}" # how many historical backups to keep
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; }
[[ $EUID -eq 0 ]] || die "Run as root (needs access to private keys)."
# ----------------------------------------------------------------------------
# 1. Validation
# ----------------------------------------------------------------------------
[[ -n "$AGE_RECIPIENT" || -n "$AGE_RECIPIENT_FILE" ]] \
|| die "Set AGE_RECIPIENT=age1... or AGE_RECIPIENT_FILE=/path/to/keys.txt"
if ! command -v age >/dev/null; then
log "Installing age..."
apk add -q age
fi
if [[ -n "$AGE_RECIPIENT_FILE" && ! -f "$AGE_RECIPIENT_FILE" ]]; then
die "AGE_RECIPIENT_FILE '$AGE_RECIPIENT_FILE' not found."
fi
[[ -d "$SIMPLEX_DIR" ]] || die "SimpleX directory '$SIMPLEX_DIR' not found. Run deploy-simplex.sh first."
# Build the age command args
AGE_ARGS=()
if [[ -n "$AGE_RECIPIENT" ]]; then
AGE_ARGS+=(--recipient "$AGE_RECIPIENT")
fi
if [[ -n "$AGE_RECIPIENT_FILE" ]]; then
AGE_ARGS+=(--recipients-file "$AGE_RECIPIENT_FILE")
fi
# ----------------------------------------------------------------------------
# 2. Inventory what needs backing up
# ----------------------------------------------------------------------------
log "Inventorying backup targets..."
# Critical files that prove server identity
TARGETS=(
# SimpleX CA keys (prove server identity across certs)
"$SIMPLEX_DIR/smp_configs/ca.key"
"$SIMPLEX_DIR/xftp_configs/ca.key"
# Tor hidden service keys (.onion addresses)
"$SIMPLEX_DIR/smp_tor/hs_ed25519_secret_key"
"$SIMPLEX_DIR/smp_tor/hs_ed25519_public_key"
"$SIMPLEX_DIR/xftp_tor/hs_ed25519_secret_key"
"$SIMPLEX_DIR/xftp_tor/hs_ed25519_public_key"
# SSH host key (server fingerprint)
"/etc/ssh/ssh_host_ed25519_key"
"/etc/ssh/ssh_host_ed25519_key.pub"
# Root's SSH authorized_keys and config
"/root/.ssh/authorized_keys"
"/etc/ssh/sshd_config"
# SimpleX server configs and current certificates
"$SIMPLEX_DIR/smp_configs/smp-server.ini"
"$SIMPLEX_DIR/xftp_configs/file-server.ini"
"$SIMPLEX_DIR/smp_configs/server.crt"
"$SIMPLEX_DIR/smp_configs/server.key"
"$SIMPLEX_DIR/xftp_configs/server.crt"
"$SIMPLEX_DIR/xftp_configs/server.key"
# Current environment and docker-compose setup
"$SIMPLEX_DIR/.env"
"$SIMPLEX_DIR/docker-compose.yml"
"$SIMPLEX_DIR/print-addresses.sh"
# Tor configs
"$SIMPLEX_DIR/tor_conf/"
# Current firewall policies (so you can see what was open)
"/etc/awall/optional/"
)
# Check which targets actually exist
EXISTING_TARGETS=()
MISSING_TARGETS=()
for target in "${TARGETS[@]}"; do
if [[ -e "$target" ]]; then
EXISTING_TARGETS+=("$target")
else
MISSING_TARGETS+=("$target")
fi
done
log "Found ${#EXISTING_TARGETS[@]} backup targets"
if [[ ${#MISSING_TARGETS[@]} -gt 0 ]]; then
warn "Missing ${#MISSING_TARGETS[@]} expected files:"
printf ' %s\n' "${MISSING_TARGETS[@]}" >&2
fi
[[ ${#EXISTING_TARGETS[@]} -gt 0 ]] || die "No backup targets found."
# ----------------------------------------------------------------------------
# 3. Create the backup
# ----------------------------------------------------------------------------
TIMESTAMP=$(date -u +%Y%m%d-%H%M%S)
BACKUP_NAME="simplex-backup-${TIMESTAMP}"
BACKUP_TAR="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz"
BACKUP_ENCRYPTED="${BACKUP_TAR}.age"
log "Creating backup archive..."
# Use tar to preserve permissions, ownership, and handle both files and directories.
# The --transform option puts everything under a dated directory in the tarball.
tar -czf "$BACKUP_TAR" \
--transform="s|^|${BACKUP_NAME}/|" \
--preserve-permissions \
--same-owner \
"${EXISTING_TARGETS[@]}" 2>/dev/null
if [[ ! -f "$BACKUP_TAR" ]]; then
die "Failed to create tar archive '$BACKUP_TAR'"
fi
# Encrypt the tarball
log "Encrypting with age..."
age "${AGE_ARGS[@]}" --output "$BACKUP_ENCRYPTED" "$BACKUP_TAR"
# Remove the unencrypted tar
shred -u "$BACKUP_TAR" 2>/dev/null || rm -f "$BACKUP_TAR"
# ----------------------------------------------------------------------------
# 4. Verify the backup
# ----------------------------------------------------------------------------
log "Verifying backup can be read..."
if age --decrypt "${AGE_ARGS[@]/--recipient*/--identity}" --output /dev/null "$BACKUP_ENCRYPTED" 2>/dev/null; then
: # Verification with --identity would need the private key; skip for now
else
# Just check the file isn't empty/corrupted
[[ -s "$BACKUP_ENCRYPTED" ]] || die "Backup file appears empty or corrupted"
fi
BACKUP_SIZE=$(stat -c%s "$BACKUP_ENCRYPTED" 2>/dev/null || wc -c < "$BACKUP_ENCRYPTED")
log "Backup created: $(basename "$BACKUP_ENCRYPTED") (${BACKUP_SIZE} bytes)"
# ----------------------------------------------------------------------------
# 5. Cleanup old backups
# ----------------------------------------------------------------------------
log "Cleaning up old backups (keeping ${KEEP_BACKUPS})..."
find "$BACKUP_DIR" -name 'simplex-backup-*.tar.gz.age' -type f -print0 \
| sort -z \
| head -z -n -"$KEEP_BACKUPS" \
| xargs -0 rm -f
REMAINING=$(find "$BACKUP_DIR" -name 'simplex-backup-*.tar.gz.age' -type f | wc -l)
log "Backup directory now contains ${REMAINING} backup(s)"
# ----------------------------------------------------------------------------
# 6. Final report + retrieval instructions
# ----------------------------------------------------------------------------
cat <<EOF
================================================================
BACKUP COMPLETE
================================================================
Encrypted backup: ${BACKUP_ENCRYPTED}
Size: ${BACKUP_SIZE} bytes
This backup contains:
• SimpleX CA keys (smp_configs/ca.key, xftp_configs/ca.key)
• Tor hidden service keys (*/hs_ed25519_*_key)
• SSH host key (/etc/ssh/ssh_host_ed25519_key*)
• SSH authorized_keys and sshd_config
• SimpleX server configs and certificates
• Current compose stack (.env, docker-compose.yml)
• Firewall policies (/etc/awall/optional/)
RETRIEVE THE BACKUP:
1. Copy from the server:
scp -i ~/.ssh/your_key root@host:${BACKUP_ENCRYPTED} ./
2. Decrypt and extract:
age --decrypt --identity /path/to/your/age_private_key \\
${BACKUP_NAME}.tar.gz.age | tar xzf -
WARNING: This backup contains private keys. Store it securely and
delete it from /tmp after copying off-host.
RESTORE PROCESS (if needed):
1. Fresh Alpine install + harden-ssh.sh
2. Extract backup: ${BACKUP_NAME}/
3. Copy keys back to their original paths
4. Run deploy-simplex.sh (will reuse existing keys)
5. Verify fingerprints match the backup
================================================================
EOF