feat(backup): implement S1 integrated backup (Q3/D4)
tools/cmd/railiance-backup-s1: - OS config snapshot: sshd, ufw, fail2ban, hosts, apt sources - installed packages list - age-encrypted, output: /opt/backup/railiance/infra/ - requires root, no network dependency Makefile: add `make backup` target Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
80
tools/cmd/railiance-backup-s1
Executable file
80
tools/cmd/railiance-backup-s1
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# tools/cmd/railiance-backup-s1 — S1 OS & Provisioning backup
|
||||
# Backs up: key OS config files applied by Ansible (sshd, ufw, fail2ban, hosts)
|
||||
# Encryption: age (reuses SOPS key pair from .sops.yaml)
|
||||
# Output: /opt/backup/railiance/infra/
|
||||
# No network required. Requires root to read /etc/.
|
||||
set -euo pipefail
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────────
|
||||
AGE_PUBLIC_KEY="age1aq8twfd78wvpra0had8cezcnj96tj4q0068edrz5jez8d6xwmflqdepsh4"
|
||||
BACKUP_DIR="/opt/backup/railiance/infra"
|
||||
KEEP=7
|
||||
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
|
||||
# Colour helpers (no external dependency)
|
||||
ok() { printf " ✅ %-12s %s\n" "$1" "$2"; }
|
||||
warn() { printf " ⚠️ %-12s %s\n" "$1" "$2"; }
|
||||
bad() { printf " ❌ %-12s %s\n" "$1" "$2"; }
|
||||
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
printf "\nrailiance-infra (S1) backup — %s\n" "${TS}"
|
||||
printf "%0.s-" $(seq 1 44); echo
|
||||
|
||||
# ── Root check ─────────────────────────────────────────────────────────────────
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
bad "root" "this script requires root — run via: sudo make backup"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── OS config snapshot ─────────────────────────────────────────────────────────
|
||||
# Captures the live state of files that Ansible manages.
|
||||
# These may drift from the playbooks if manual changes were made.
|
||||
ok "os-config" "snapshotting /etc config…"
|
||||
|
||||
OS_FILES=(
|
||||
/etc/ssh/sshd_config
|
||||
/etc/ssh/sshd_config.d/
|
||||
/etc/ufw/ufw.conf
|
||||
/etc/ufw/user.rules
|
||||
/etc/ufw/user6.rules
|
||||
/etc/fail2ban/jail.local
|
||||
/etc/fail2ban/jail.d/
|
||||
/etc/hosts
|
||||
/etc/hostname
|
||||
/etc/apt/sources.list.d/
|
||||
)
|
||||
|
||||
TMP_OS="$(mktemp -d)"
|
||||
for item in "${OS_FILES[@]}"; do
|
||||
[[ -e "${item}" ]] || continue
|
||||
dest="${TMP_OS}$(dirname "${item}")"
|
||||
mkdir -p "${dest}"
|
||||
cp -a "${item}" "${dest}/" 2>/dev/null || true
|
||||
done
|
||||
|
||||
tar -czf - -C "${TMP_OS}" . \
|
||||
| age -r "${AGE_PUBLIC_KEY}" -o "${BACKUP_DIR}/os-config-${TS}.tar.gz.age"
|
||||
rm -rf "${TMP_OS}"
|
||||
ok "os-config" "encrypted → os-config-${TS}.tar.gz.age"
|
||||
|
||||
# ── Installed packages list ────────────────────────────────────────────────────
|
||||
if command -v dpkg &>/dev/null; then
|
||||
dpkg --get-selections \
|
||||
| age -r "${AGE_PUBLIC_KEY}" -o "${BACKUP_DIR}/packages-${TS}.txt.age"
|
||||
ok "packages" "encrypted → packages-${TS}.txt.age"
|
||||
fi
|
||||
|
||||
# ── Prune local cache ──────────────────────────────────────────────────────────
|
||||
for pattern in "os-config-*.tar.gz.age" "packages-*.txt.age"; do
|
||||
find "${BACKUP_DIR}" -name "${pattern}" | sort -r | tail -n +$((KEEP + 1)) | xargs -r rm -f
|
||||
done
|
||||
ok "prune" "kept last ${KEEP} of each type"
|
||||
|
||||
# ── Stamp ──────────────────────────────────────────────────────────────────────
|
||||
echo "${TS}" > "${BACKUP_DIR}/.last-backup"
|
||||
|
||||
echo
|
||||
ok "done" "backup complete — ${TS}"
|
||||
echo " Location: ${BACKUP_DIR}"
|
||||
echo " Decrypt with: age -d -i ~/.config/sops/age/keys.txt <file>"
|
||||
Reference in New Issue
Block a user