[object Object]

← back to Secrets Manager

durable daily post-bubbe Tier-1 secret-rotation reminder

89bebbabc8ba4741d2ccfca2b8353bbe98f6d315 · 2026-05-31 16:43:51 -0700 · Steve Abrams

LaunchAgent-driven (com.steve.postbubbe-rotation-reminder, 8:37am daily,
survives restarts) replacement for the session-only cron. Compares each
Tier-1 key's registry digest against the 2026-05-31 baseline; a changed
digest = rotated. Fires a macOS notification listing keys still un-rotated,
and SELF-RETIRES (boots out the agent + drops a RESOLVED marker) once all 9
have been rotated. Re-verify + remind only — never rotates console-only keys.

Tracks: the reminder script + the digest baseline (digests are last4:sha-prefix,
not secrets). Plist lives in ~/Library/LaunchAgents (outside the repo).

Files touched

Diff

commit 89bebbabc8ba4741d2ccfca2b8353bbe98f6d315
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 31 16:43:51 2026 -0700

    durable daily post-bubbe Tier-1 secret-rotation reminder
    
    LaunchAgent-driven (com.steve.postbubbe-rotation-reminder, 8:37am daily,
    survives restarts) replacement for the session-only cron. Compares each
    Tier-1 key's registry digest against the 2026-05-31 baseline; a changed
    digest = rotated. Fires a macOS notification listing keys still un-rotated,
    and SELF-RETIRES (boots out the agent + drops a RESOLVED marker) once all 9
    have been rotated. Re-verify + remind only — never rotates console-only keys.
    
    Tracks: the reminder script + the digest baseline (digests are last4:sha-prefix,
    not secrets). Plist lives in ~/Library/LaunchAgents (outside the repo).
---
 postbubbe-rotation-reminder.sh | 62 ++++++++++++++++++++++++++++++++++++++++++
 postbubbe-tier1-baseline.json  | 42 ++++++++++++++++++++++++++++
 2 files changed, 104 insertions(+)

diff --git a/postbubbe-rotation-reminder.sh b/postbubbe-rotation-reminder.sh
new file mode 100755
index 0000000..45d8370
--- /dev/null
+++ b/postbubbe-rotation-reminder.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+# postbubbe-rotation-reminder.sh
+# Durable daily reminder for the OUTSTANDING post-bubbe-RCE Tier-1 secret rotation.
+# Compares each Tier-1 key's current registry digest against the 2026-05-31
+# baseline; a key is ROTATED when its digest changes (Steve regenerates in the
+# provider console, then `cli.js import-paste` rewrites the digest).
+#   - any still un-rotated  -> macOS notification + log line, exit 0
+#   - ALL rotated           -> success notification, SELF-RETIRE (unload + disable
+#                              the LaunchAgent), drop a RESOLVED marker, exit 0
+# Self-contained, no args. Driven by com.steve.postbubbe-rotation-reminder.
+set -uo pipefail
+
+SM="$HOME/Projects/secrets-manager"
+BASELINE="$SM/postbubbe-tier1-baseline.json"
+REGISTRY="$SM/registry.json"
+LOG="$SM/postbubbe-rotation-reminder.log"
+PLIST="$HOME/Library/LaunchAgents/com.steve.postbubbe-rotation-reminder.plist"
+LABEL="com.steve.postbubbe-rotation-reminder"
+MARKER="$SM/.postbubbe-rotation-RESOLVED"
+STAMP="$(date '+%Y-%m-%d %H:%M:%S')"
+
+note(){ printf '[%s] %s\n' "$STAMP" "$*" >> "$LOG"; }
+notify(){ # title, message
+  /usr/bin/osascript -e "display notification \"$2\" with title \"$1\" sound name \"Glass\"" >/dev/null 2>&1 || true
+}
+
+[ -f "$BASELINE" ] || { note "ERROR baseline missing: $BASELINE"; exit 0; }
+[ -f "$REGISTRY" ] || { note "ERROR registry missing: $REGISTRY"; exit 0; }
+
+# Compare digests in node (jq may be absent); print "ROTATED a b" / "PENDING x y".
+RESULT="$(BASELINE="$BASELINE" REGISTRY="$REGISTRY" /usr/bin/env node -e '
+const fs=require("fs");
+const base=JSON.parse(fs.readFileSync(process.env.BASELINE,"utf8")).keys||{};
+const reg=JSON.parse(fs.readFileSync(process.env.REGISTRY,"utf8")).secrets||{};
+const pending=[], rotated=[];
+for(const k of Object.keys(base)){
+  const was=base[k].digest||null;
+  const now=(reg[k]&&reg[k].digest)||null;
+  if(now && was && now!==was) rotated.push(k); else pending.push(k);
+}
+console.log("PENDING\t"+pending.join(","));
+console.log("ROTATED\t"+rotated.join(","));
+' 2>>"$LOG")"
+
+PENDING="$(printf '%s\n' "$RESULT" | awk -F'\t' '/^PENDING/{print $2}')"
+ROTATED="$(printf '%s\n' "$RESULT" | awk -F'\t' '/^ROTATED/{print $2}')"
+PCOUNT=0; [ -n "$PENDING" ] && PCOUNT="$(printf '%s' "$PENDING" | tr ',' '\n' | grep -c .)"
+
+if [ "$PCOUNT" -eq 0 ]; then
+  note "ALL Tier-1 keys rotated ($ROTATED). Retiring reminder."
+  notify "✅ Post-bubbe rotation complete" "All Tier-1 keys rotated. Retiring the daily reminder."
+  printf 'RESOLVED %s\nrotated: %s\n' "$STAMP" "$ROTATED" > "$MARKER"
+  # Self-retire the LaunchAgent (modern bootout, fall back to legacy unload).
+  launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || launchctl unload "$PLIST" 2>/dev/null || true
+  # Disable RunAtLoad/relaunch by renaming the plist aside so it won't reload.
+  [ -f "$PLIST" ] && mv "$PLIST" "$PLIST.retired-$(date +%Y%m%d)" 2>/dev/null || true
+  exit 0
+fi
+
+note "STILL OUTSTANDING ($PCOUNT): $PENDING"
+notify "🔒 Rotate $PCOUNT post-bubbe keys" "Still live & unrotated: ${PENDING}. Regenerate in console → cli.js import-paste. Order: CF/GoDaddy/Stripe/Shopify → OpenAI/Gemini/ElevenLabs."
+exit 0
diff --git a/postbubbe-tier1-baseline.json b/postbubbe-tier1-baseline.json
new file mode 100644
index 0000000..2d04aaa
--- /dev/null
+++ b/postbubbe-tier1-baseline.json
@@ -0,0 +1,42 @@
+{
+  "captured_at": "2026-05-31",
+  "note": "post-bubbe RCE Tier-1 baseline digests; key is ROTATED when its registry digest differs from this value",
+  "keys": {
+    "STRIPE_SECRET_KEY": {
+      "digest": "8LQ0:fe981600",
+      "last_updated": "2026-05-01T06:23:30.081Z"
+    },
+    "CLOUDFLARE_API_TOKEN": {
+      "digest": "e700:3609b6eb",
+      "last_updated": "2026-05-18T19:25:46.508Z"
+    },
+    "GODADDY_API_KEY": {
+      "digest": "acsF:96974d0c",
+      "last_updated": "2026-04-30T18:50:35.250Z"
+    },
+    "GODADDY_API_SECRET": {
+      "digest": "Vnej:31492271",
+      "last_updated": "2026-04-30T18:50:40.119Z"
+    },
+    "SHOPIFY_ADMIN_TOKEN": {
+      "digest": "75b9:95b01e9b",
+      "last_updated": "2026-05-07T23:26:22.519Z"
+    },
+    "SHOPIFY_DW_ADMIN_TOKEN_CONTENT": {
+      "digest": "19b4:8d306dfc",
+      "last_updated": "2026-05-08T06:19:32.976Z"
+    },
+    "OPENAI_API_KEY": {
+      "digest": "FYwA:8ad69c60",
+      "last_updated": "2026-05-01T05:54:19.819Z"
+    },
+    "GEMINI_API_KEY": {
+      "digest": "k720:4295091b",
+      "last_updated": "2026-05-02T15:34:58.966Z"
+    },
+    "ELEVENLABS_API_KEY": {
+      "digest": "365a:7f1a7de6",
+      "last_updated": "2026-05-06T21:37:52.553Z"
+    }
+  }
+}
\ No newline at end of file

← a1e5d79 gitignore: add patterns for backup/scratch files (*.bak, *.b  ·  back to Secrets Manager  ·  Add credential rotation checklist (dw_admin pw + 2 Gemini ke 4fbb01c →