[object Object]

← back to Secrets Manager

feat: backup-env.sh — encrypted off-box backup of canonical .env (ciphertext-only to gdrive); dry-run default, --apply gated

f342dbe8705bd00b7c8b3cc9b05a72438a8e35cc · 2026-06-15 23:43:13 -0700 · SteveStudio2

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit f342dbe8705bd00b7c8b3cc9b05a72438a8e35cc
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon Jun 15 23:43:13 2026 -0700

    feat: backup-env.sh — encrypted off-box backup of canonical .env (ciphertext-only to gdrive); dry-run default, --apply gated
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 scripts/backup-env.sh | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 61 insertions(+)

diff --git a/scripts/backup-env.sh b/scripts/backup-env.sh
new file mode 100644
index 0000000..c7cd3f1
--- /dev/null
+++ b/scripts/backup-env.sh
@@ -0,0 +1,61 @@
+#!/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."

← e1b76f7 retire Desktop secret mirror (~/Desktop/site-factory.env) —  ·  back to Secrets Manager  ·  Add automated encrypted off-box backup of secrets master (.e da036c7 →