#!/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 <