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
226 lines
8.0 KiB
Bash
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
|