← back to Secrets Manager
backup-env.sh
90 lines
#!/bin/bash
# backup-env.sh — automated, versioned, ENCRYPTED, OFF-BOX backup of the secrets master.
# Born 2026-06-16 (Officer Council #1 leftover): secrets-manager/.env had ZERO automated
# backup — one disk loss = total secret loss (memory: dw-unified-backup-broken).
#
# What it does, every run:
# 1. GPG-symmetric (AES256) encrypts .env -> timestamped snapshot in ~/.secrets-backups
# 2. ROUND-TRIP VERIFIES (decrypt the fresh snapshot, diff vs source) — proves restorable
# 3. Mirrors the latest encrypted blob to iCloud Drive (off-box: survives Mac2 disk loss)
# 4. Retains last 30 local snapshots; prunes older
# Passphrase lives in the macOS login Keychain (machine-bound). On FIRST run it is generated
# and an ESCROW handoff file is written for Steve to move into his password manager — because
# if Mac2 dies, the iCloud ciphertext is only decryptable with that passphrase.
#
# READ + ENCRYPT only. Never writes/rotates the actual secrets. Never commits .env.
set -uo pipefail
SRC="$HOME/Projects/secrets-manager/.env"
DST="$HOME/.secrets-backups"; mkdir -p "$DST"; chmod 700 "$DST"
LOG="$DST/backup.log"
ICLOUD="$HOME/Library/Mobile Documents/com~apple~CloudDocs/secrets-backups"
KC_SERVICE="secrets-env-backup-key"
KEEP=30
log(){ echo "[$(date -Iseconds)] $1" | tee -a "$LOG"; }
[ -f "$SRC" ] || { log "FATAL: source $SRC missing"; exit 1; }
# --- passphrase: load from Keychain, generate + escrow on first run ---
PASS="$(security find-generic-password -a "$USER" -s "$KC_SERVICE" -w 2>/dev/null || true)"
if [ -z "$PASS" ]; then
PASS="$(openssl rand -base64 48 | tr -d '\n')"
security add-generic-password -a "$USER" -s "$KC_SERVICE" -w "$PASS" -U 2>/dev/null \
&& log "generated new backup passphrase, stored in login Keychain ($KC_SERVICE)" \
|| { log "FATAL: could not store passphrase in Keychain"; exit 1; }
ESC="$DST/KEY-ESCROW-$(date +%Y%m%d-%H%M%S).txt"
{ echo "SECRETS BACKUP PASSPHRASE — ESCROW THIS, THEN DELETE THIS FILE.";
echo "Move into your password manager. Without it, the iCloud encrypted backups are";
echo "UNRECOVERABLE if this Mac is lost (the key only lives in this Mac's Keychain).";
echo "service=$KC_SERVICE account=$USER";
echo "passphrase=$PASS"; } > "$ESC"
chmod 600 "$ESC"
log "ACTION REQUIRED: escrow file written $ESC (chmod 600) — Steve must save the passphrase + delete the file"
fi
# --- encrypt ---
TS="$(date +%Y%m%d-%H%M%S)"
OUT="$DST/secrets-env-$TS.env.gpg"
if ! gpg --batch --yes --passphrase "$PASS" -c --cipher-algo AES256 -o "$OUT" "$SRC" 2>>"$LOG"; then
log "FATAL: gpg encrypt failed"; exit 1
fi
chmod 600 "$OUT"
# --- round-trip verify (the whole point: prove it's restorable) ---
if gpg --batch --yes --passphrase "$PASS" -d "$OUT" 2>/dev/null | diff -q - "$SRC" >/dev/null; then
log "VERIFIED restorable: $OUT ($(wc -c <"$OUT" | tr -d ' ') bytes, decrypt==source)"
else
log "FATAL: round-trip verify FAILED for $OUT — removing bad snapshot"; rm -f "$OUT"; exit 1
fi
# --- off-box mirror (iCloud) ---
# The TIMESTAMPED copy is the real off-box backup; the 'latest' pointer is convenience.
# Overwriting an already-synced iCloud file from a launchd context can hit "Operation not
# permitted" (TCC), so the pointer is best-effort and never fails the run.
if [ -d "$(dirname "$ICLOUD")" ]; then
mkdir -p "$ICLOUD"
if cp "$OUT" "$ICLOUD/"; then
log "off-box mirror -> iCloud Drive ($ICLOUD/$(basename "$OUT"))"
# best-effort 'latest' pointer (rm+cp; ignore TCC failure on overwrite)
{ rm -f "$ICLOUD/secrets-env-latest.env.gpg" 2>/dev/null; cp "$OUT" "$ICLOUD/secrets-env-latest.env.gpg" 2>/dev/null; } || true
else
log "WARN: iCloud off-box copy failed (Operation not permitted? grant the launchd job Full Disk Access) — local encrypted snapshots still good"
fi
else
log "WARN: iCloud Drive not present — local snapshots only (no off-box copy this run)"
fi
# --- retention ---
COUNT=$(ls -1 "$DST"/secrets-env-*.env.gpg 2>/dev/null | wc -l | tr -d ' ')
if [ "$COUNT" -gt "$KEEP" ]; then
ls -1t "$DST"/secrets-env-*.env.gpg | tail -n +$((KEEP+1)) | while read -r f; do rm -f "$f"; done
log "retention: pruned to last $KEEP (was $COUNT)"
fi
# Mirror retention on iCloud too
if [ -d "$ICLOUD" ]; then
IC=$(ls -1 "$ICLOUD"/secrets-env-2*.env.gpg 2>/dev/null | wc -l | tr -d ' ')
[ "$IC" -gt "$KEEP" ] && ls -1t "$ICLOUD"/secrets-env-2*.env.gpg | tail -n +$((KEEP+1)) | while read -r f; do rm -f "$f"; done
fi
log "OK: backed up secrets master ($(wc -c <"$SRC" | tr -d ' ') bytes) -> $COUNT local snapshot(s) + iCloud"
exit 0