← back to Secrets Manager

scripts/backup-env.sh

62 lines

#!/bin/bash
# backup-env.sh — encrypted off-box backup of the canonical secret master.
#
# The canonical .env (~/Projects/secrets-manager/.env) has had ZERO automated
# backup since the Desktop mirror was retired (memory: desktop-secret-mirror-retired).
# One disk loss = total secret loss. This gpg-symmetric-encrypts the .env and
# pushes the CIPHERTEXT (never plaintext) to gdrive:secrets-backup/.
#
# SAFETY:
#   * Plaintext NEVER leaves the box — only the .gpg ciphertext is uploaded.
#   * Passphrase comes from $SECRETS_BACKUP_PASSPHRASE or ~/.config/secrets-backup.pass
#     (chmod 600). Without it the script refuses to run.
#   * DRY-RUN by default; --apply performs the real encrypt+upload.
#   * Keeps the last 14 dated copies remotely; never deletes the source.
#
# USAGE:  bash backup-env.sh            # dry-run (shows what it WOULD do)
#         bash backup-env.sh --apply    # encrypt + upload  (Steve-gated first run)
set -uo pipefail

ENV_SRC="$HOME/Projects/secrets-manager/.env"
REMOTE="gdrive:secrets-backup"
PASS_FILE="$HOME/.config/secrets-backup.pass"
STAMP="$(date +%Y%m%d-%H%M%S)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
APPLY=0; [ "${1:-}" = "--apply" ] && APPLY=1

[ -f "$ENV_SRC" ] || { echo "⛔ no .env at $ENV_SRC"; exit 1; }

# resolve passphrase
PASS="${SECRETS_BACKUP_PASSPHRASE:-}"
if [ -z "$PASS" ] && [ -f "$PASS_FILE" ]; then PASS="$(cat "$PASS_FILE")"; fi
if [ -z "$PASS" ]; then
  echo "⛔ no passphrase. Set \$SECRETS_BACKUP_PASSPHRASE or create $PASS_FILE (chmod 600)."
  exit 2
fi

SIZE=$(wc -c < "$ENV_SRC" | tr -d ' ')
OUT="secrets-env-$STAMP.env.gpg"
echo "source: $ENV_SRC ($SIZE bytes)"
echo "target: $REMOTE/$OUT  (gpg AES256, ciphertext only)"

if [ "$APPLY" -eq 0 ]; then
  echo "DRY-RUN — no encryption, no upload. Re-run with --apply to execute."
  echo "(retention: keep newest 14 remote copies)"
  exit 0
fi

# encrypt to ciphertext in tmp
printf '%s' "$PASS" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 \
  -c --cipher-algo AES256 -o "$TMP/$OUT" "$ENV_SRC" || { echo "⛔ gpg failed"; exit 3; }
echo "encrypted: $(wc -c < "$TMP/$OUT" | tr -d ' ') bytes"

# upload ciphertext
rclone copy "$TMP/$OUT" "$REMOTE/" || { echo "⛔ rclone upload failed"; exit 4; }
echo "✓ uploaded $REMOTE/$OUT"

# prune remote to newest 14
mapfile -t OLD < <(rclone lsf "$REMOTE/" --include "secrets-env-*.env.gpg" 2>/dev/null | sort | head -n -14)
for f in "${OLD[@]:-}"; do [ -n "$f" ] && rclone deletefile "$REMOTE/$f" && echo "pruned $f"; done
echo "done."