[object Object]

← back to Secrets Manager

credentials/tokens update tracker web viewer (:9778) + dw_admin rotation pre-stage doc

c35aa92ea0b6b7d27d82b6dfb5c87dd31ba36786 · 2026-06-01 14:30:20 -0700 · Steve Abrams

Files touched

Diff

commit c35aa92ea0b6b7d27d82b6dfb5c87dd31ba36786
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 1 14:30:20 2026 -0700

    credentials/tokens update tracker web viewer (:9778) + dw_admin rotation pre-stage doc
---
 ROTATION-PRESTAGE.md            |  49 ++++++++++++++++++++
 update-viewer/index.html        |  92 ++++++++++++++++++++++++++++++++++++
 update-viewer/server.mjs        |  31 +++++++++++++
 update-viewer/update-items.json | 100 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 272 insertions(+)

diff --git a/ROTATION-PRESTAGE.md b/ROTATION-PRESTAGE.md
new file mode 100644
index 0000000..de3cbf5
--- /dev/null
+++ b/ROTATION-PRESTAGE.md
@@ -0,0 +1,49 @@
+# dw_admin Rotation — Pre-Stage (ready for Steve's console window)
+
+> Prepared 2026-06-01. Goal: shrink the hands-on window to ~10 min so Kamatera
+> can be cleared for the 65-site DW fleet rebuild deploy. **No secret VALUES here.**
+> Claude has done all the code prep; the steps below are the console-only actions
+> only Steve can run (mint pw, ALTER ROLE, restart prod).
+
+---
+
+## ✅ Code prep DONE (by Claude, on branches — not deployed)
+
+- `archive-agent/ecosystem.config.js` — DSN already env-first (no pw literal). ✔
+- `Designer-Wallcoverings/DW-Agents/ecosystem.config.js:134` — **fixed → env-first**
+  (`process.env.DATABASE_URL || '…dw_admin@127.0.0.1…'`), committed on `rotation-prestage`.
+  → both inline-DSN gotchas from the runbook are resolved; a restart now picks up
+  the rotated pw from `DATABASE_URL` env with **no further config edits**.
+- gitleaks pre-commit hooks + secret-strip already removed the cleartext pw from
+  tracked source fleet-wide (origin incident).
+
+## Console steps (Steve — ~10 min)
+
+1. **Mint** a new ≥20-char password (1Password / `openssl rand -base64 24`).
+2. **ALTER ROLE on BOTH** databases:
+   - **Prod Kamatera:** `ssh root@45.61.58.125` → `sudo -u postgres psql -c "ALTER ROLE dw_admin WITH PASSWORD '<NEW>';"`
+   - **Local Mac2:** `psql -U postgres -c "ALTER ROLE dw_admin WITH PASSWORD '<NEW>';"`
+3. **Route back** (fans the new pw to every registered .env + MCP env):
+   ```
+   printf 'DW_ADMIN_DB_PASSWORD=<NEW>' | node ~/Projects/secrets-manager/cli.js import-paste
+   ```
+4. **Restart consumers** so they reconnect with the new pw:
+   - **Local Mac2:** `pm2 restart wallco-ai-9905` (the local PG consumer); others (run-notes-chat / claude-control-center / morning-review) don't hit dw_admin.
+   - **Kamatera:** `ssh root@45.61.58.125 'pm2 restart all --update-env'` — the ~40 dw_admin pm2 procs (DW-Agents ×14, Norma ×8, pd-* ×7, site-factory, directories, dw-commerce hub). `--update-env` is required so they re-read the rotated `DATABASE_URL`.
+5. **Verify:**
+   - `node ~/Projects/secrets-manager/cli.js check` (validates the new pw connects)
+   - `ssh root@45.61.58.125 'pm2 ls'` → all `online`, no restart loops
+   - spot-check one prod app responds (e.g. `curl -s -o /dev/null -w "%{http_code}" https://designerwallcoverings.com`)
+
+## After rotation → Kamatera is clear
+- Flip the cncp dashboard prod flag off (`server.js:1718` `status:'compromised'` → `ok`).
+- Then the DW fleet rebuild can deploy (canary `1890swallpaper` → parallel the other 64).
+
+## Still outstanding (separate from dw_admin, but same incident)
+- Gemini API key #1 (7 Patty routes + 3 DW docs) — mint new at aistudio.google.com, delete old, route back.
+- Gemini API key #2 (`PoppyPetitions/lib/gemini.ts`) — same.
+- Norma scoped-user rotation (DTD-cleared, local-only) — does NOT fix dw_admin; optional now.
+
+## What Claude can do post-rotation (no secrets)
+- Run step 3's route-back + step 5 verification.
+- Drive the canary + parallel deploy once you flip the prod flag.
diff --git a/update-viewer/index.html b/update-viewer/index.html
new file mode 100644
index 0000000..7c59bf2
--- /dev/null
+++ b/update-viewer/index.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Credentials & Tokens — Update Tracker</title>
+<style>
+  :root{--bg:#0d0f13;--panel:#161a21;--ink:#e9e7e2;--muted:#8b919c;--rule:#262b34;--p1:#ef4444;--p2:#fbbf24;--ok:#4ade80;--accent:#9c7a4d}
+  *{box-sizing:border-box;margin:0;padding:0}
+  body{background:var(--bg);color:var(--ink);font-family:'Inter',system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased;padding:0 0 60px}
+  header{position:sticky;top:0;z-index:5;background:rgba(13,15,19,.95);backdrop-filter:blur(8px);border-bottom:1px solid var(--rule);padding:18px 28px}
+  h1{font-family:'Cormorant Garamond',Georgia,serif;font-weight:600;font-size:27px;letter-spacing:.02em}
+  .sub{color:var(--muted);font-size:13px;margin-top:3px}
+  .counts{display:flex;gap:10px;margin-top:12px;flex-wrap:wrap}
+  .pill{font-size:12px;padding:4px 11px;border-radius:999px;border:1px solid var(--rule);color:var(--muted)}
+  .pill b{color:var(--ink)}
+  .wrap{max-width:980px;margin:24px auto;padding:0 24px;display:flex;flex-direction:column;gap:16px}
+  .card{background:var(--panel);border:1px solid var(--rule);border-radius:12px;padding:18px 20px;border-left:4px solid var(--rule)}
+  .card.P1{border-left-color:var(--p1)} .card.P2{border-left-color:var(--p2)} .card.prep{border-left-color:var(--muted)} .card.done{opacity:.62}
+  .top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
+  .title{font-size:17px;font-weight:600}
+  .sev{font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;padding:2px 8px;border-radius:5px;border:1px solid currentColor}
+  .sev.P1{color:var(--p1)}.sev.P2{color:var(--p2)}.sev.prep,.sev.P3{color:var(--muted)}
+  .type{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
+  .togg{margin-left:auto;font-size:12px;font-weight:600;padding:6px 13px;border-radius:7px;border:1px solid var(--rule);background:transparent;color:var(--muted);cursor:pointer}
+  .togg.outstanding{color:var(--p2);border-color:#fbbf2455}
+  .togg.done{color:var(--ok);border-color:#4ade8055;background:#4ade8011}
+  .gates{margin-top:8px;font-size:12px;color:var(--p1);background:#ef444411;border:1px solid #ef444433;border-radius:6px;padding:6px 10px;display:inline-block}
+  .exposed{margin-top:10px;font-size:12px;color:var(--muted);line-height:1.5}
+  .exposed b{color:#cbb}
+  .steps{margin-top:12px;display:flex;flex-direction:column;gap:7px}
+  .step{display:flex;gap:9px;align-items:flex-start;font-size:12px;color:#c7ccd4}
+  .step .n{color:var(--accent);font-weight:700;flex-shrink:0}
+  .cmd{margin-top:6px;background:#0a0c10;border:1px solid var(--rule);border-radius:6px;padding:9px 11px;font-family:'SF Mono',ui-monospace,monospace;font-size:11.5px;color:#9fd;white-space:pre-wrap;word-break:break-all;position:relative;cursor:pointer}
+  .cmd:hover{border-color:var(--accent)}
+  .cmd .lbl{position:absolute;top:6px;right:9px;font-size:9px;color:var(--muted);letter-spacing:.1em;text-transform:uppercase}
+  .mint{display:inline-block;margin-top:10px;font-size:12px;color:#7dd3fc;text-decoration:none;border:1px solid #7dd3fc44;border-radius:6px;padding:5px 11px}
+  .mint:hover{background:#7dd3fc11}
+  .runnable{margin-top:10px;font-size:11px;color:var(--ok)}
+  .row-label{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:14px 0 2px}
+</style>
+</head>
+<body>
+<header>
+  <h1>Credentials &amp; Tokens — Update Tracker</h1>
+  <div class="sub" id="origin">loading…</div>
+  <div class="counts" id="counts"></div>
+</header>
+<div class="wrap" id="wrap"></div>
+<script>
+const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+function cmdBox(label,txt){ if(!txt)return ''; return '<div class="cmd" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.dataset.c)" data-c="'+esc(txt)+'"><span class="lbl">'+label+' · copy</span>'+esc(txt)+'</div>'; }
+async function load(){
+  const d=await (await fetch('/api/items')).json();
+  document.getElementById('origin').textContent=d.origin;
+  const out=d.items.filter(i=>i.status==='outstanding');
+  const p1=out.filter(i=>i.severity==='P1').length;
+  document.getElementById('counts').innerHTML=
+    '<span class="pill"><b>'+out.length+'</b> outstanding</span>'+
+    '<span class="pill" style="color:var(--p1)"><b>'+p1+'</b> P1 (blocks Kamatera)</span>'+
+    '<span class="pill"><b>'+d.items.filter(i=>i.status==='done').length+'</b> done</span>';
+  const w=document.getElementById('wrap'); w.innerHTML='';
+  for(const it of d.items){
+    const div=document.createElement('div');
+    div.className='card '+it.severity+(it.status==='done'?' done':'');
+    const steps=(it.console||[]).map((s,i)=>'<div class="step"><span class="n">'+(i+1)+'</span><span>'+esc(s)+'</span></div>').join('');
+    div.innerHTML=
+      '<div class="top">'+
+        '<span class="sev '+it.severity+'">'+esc(it.severity)+'</span>'+
+        '<span class="title">'+esc(it.title)+'</span>'+
+        '<span class="type">'+esc(it.type)+'</span>'+
+        '<button class="togg '+it.status+'" data-id="'+it.id+'" data-st="'+it.status+'">'+(it.status==='done'?'✓ done':'mark done')+'</button>'+
+      '</div>'+
+      (it.gates?'<div class="gates">⛔ gates: '+esc(it.gates)+'</div>':'')+
+      (it.exposed?'<div class="exposed"><b>Exposed:</b> '+esc(it.exposed)+'</div>':'')+
+      (steps?'<div class="row-label">Console (Steve)</div><div class="steps">'+steps+'</div>':'')+
+      (it.mintUrl?'<a class="mint" target="_blank" rel="noopener" href="'+esc(it.mintUrl)+'">↗ mint new key</a>':'')+
+      (it.routeBack?'<div class="row-label">Route back</div>'+cmdBox('route',it.routeBack):'')+
+      (it.restart?'<div class="row-label">Restart</div>'+cmdBox('restart',it.restart):'')+
+      (it.verify?'<div class="row-label">Verify</div>'+cmdBox('verify',it.verify):'')+
+      ((it.claudeCanRun&&it.claudeCanRun.length)?'<div class="runnable">⚡ Claude can run: '+it.claudeCanRun.join(', ')+'</div>':'');
+    w.appendChild(div);
+  }
+  w.querySelectorAll('.togg').forEach(b=>b.onclick=async()=>{
+    const ns=b.dataset.st==='done'?'outstanding':'done';
+    await fetch('/api/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:b.dataset.id,status:ns})});
+    load();
+  });
+}
+load();
+</script>
+</body>
+</html>
diff --git a/update-viewer/server.mjs b/update-viewer/server.mjs
new file mode 100644
index 0000000..d59bea7
--- /dev/null
+++ b/update-viewer/server.mjs
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+// Credentials & Tokens Update Tracker — local web viewer. Zero deps.
+//   node server.mjs   → http://localhost:9778
+// Reads update-items.json; lets Steve toggle status (persisted). No secret VALUES.
+import http from 'node:http';
+import fs from 'node:fs';
+import path from 'node:path';
+
+const DIR = path.dirname(new URL(import.meta.url).pathname);
+const DATA = path.join(DIR, 'update-items.json');
+const PORT = process.env.PORT || 9778;
+const send = (res, code, type, body) => { res.writeHead(code, { 'Content-Type': type, 'Cache-Control': 'no-store' }); res.end(body); };
+
+http.createServer((req, res) => {
+  const u = new URL(req.url, 'http://localhost');
+  if (u.pathname === '/') return send(res, 200, 'text/html; charset=utf-8', fs.readFileSync(path.join(DIR, 'index.html')));
+  if (u.pathname === '/api/items') return send(res, 200, 'application/json', fs.readFileSync(DATA));
+  if (u.pathname === '/api/toggle' && req.method === 'POST') {
+    let b = ''; req.on('data', c => b += c); req.on('end', () => {
+      try {
+        const { id, status } = JSON.parse(b);
+        const d = JSON.parse(fs.readFileSync(DATA, 'utf8'));
+        const it = d.items.find(x => x.id === id);
+        if (it) { it.status = status; fs.writeFileSync(DATA, JSON.stringify(d, null, 2) + '\n'); }
+        send(res, 200, 'application/json', JSON.stringify({ ok: !!it }));
+      } catch (e) { send(res, 400, 'application/json', JSON.stringify({ ok: false, error: e.message })); }
+    });
+    return;
+  }
+  send(res, 404, 'text/plain', 'not found');
+}).listen(PORT, () => console.log(`Credentials Update Tracker → http://localhost:${PORT}`));
diff --git a/update-viewer/update-items.json b/update-viewer/update-items.json
new file mode 100644
index 0000000..8755dc8
--- /dev/null
+++ b/update-viewer/update-items.json
@@ -0,0 +1,100 @@
+{
+  "title": "Credentials & Tokens — Update Tracker",
+  "origin": "2026-05-30 Kamatera compromise + ongoing token hygiene",
+  "items": [
+    {
+      "id": "dw-admin-pg",
+      "title": "PostgreSQL dw_admin password",
+      "type": "db-password",
+      "severity": "P1",
+      "status": "outstanding",
+      "gates": "Kamatera deploy (the do-not-deploy flag clears only after this)",
+      "exposed": "hardcoded cleartext in 14+ files (scrubbed); Time Machine / Spotlight / possibly remote git history",
+      "mintUrl": null,
+      "console": [
+        "Mint new ≥20-char pw (openssl rand -base64 24)",
+        "Kamatera: ssh root@45.61.58.125 → sudo -u postgres psql -c \"ALTER ROLE dw_admin WITH PASSWORD '<NEW>';\"",
+        "Local Mac2: psql -U postgres -c \"ALTER ROLE dw_admin WITH PASSWORD '<NEW>';\""
+      ],
+      "routeBack": "printf 'DW_ADMIN_DB_PASSWORD=<NEW>' | node ~/Projects/secrets-manager/cli.js import-paste",
+      "restart": "local: pm2 restart wallco-ai-9905  |  Kamatera: pm2 restart all --update-env",
+      "verify": "node ~/Projects/secrets-manager/cli.js check",
+      "claudeCanRun": ["routeBack", "restart-local", "verify"]
+    },
+    {
+      "id": "gemini-1",
+      "title": "Gemini API key #1",
+      "type": "api-key",
+      "severity": "P2",
+      "status": "outstanding",
+      "gates": null,
+      "exposed": "hardcoded in 7 Patty routes + 3 DW docs",
+      "mintUrl": "https://aistudio.google.com/apikey",
+      "console": ["Create new key, DELETE the old one in the same console"],
+      "routeBack": "printf 'GEMINI_API_KEY=<NEW>' | node ~/Projects/secrets-manager/cli.js import-paste",
+      "restart": null,
+      "verify": "node ~/Projects/secrets-manager/cli.js check  (hits Gemini /models)",
+      "claudeCanRun": ["routeBack", "verify"]
+    },
+    {
+      "id": "gemini-2",
+      "title": "Gemini API key #2",
+      "type": "api-key",
+      "severity": "P2",
+      "status": "outstanding",
+      "gates": null,
+      "exposed": "hardcoded in PoppyPetitions/lib/gemini.ts",
+      "mintUrl": "https://aistudio.google.com/apikey",
+      "console": ["Create new key, delete old", "confirm KEY_NAME against routes.json (GEMINI_API_KEY_RALPH or poppy-specific)"],
+      "routeBack": "printf '<KEY_NAME>=<NEW>' | node ~/Projects/secrets-manager/cli.js import-paste",
+      "restart": null,
+      "verify": "node ~/Projects/secrets-manager/cli.js check",
+      "claudeCanRun": ["routeBack", "verify"]
+    },
+    {
+      "id": "norma-scoped",
+      "title": "Norma sdcc scoped PG user (norma_app_user)",
+      "type": "db-scoped-role",
+      "severity": "P3 (optional)",
+      "status": "outstanding",
+      "gates": null,
+      "exposed": "Norma currently connects as shared dw_admin on local sdcc; this moves it to least-privilege (does NOT fix dw_admin)",
+      "mintUrl": null,
+      "console": ["~/Projects/_shared/scripts/rotate-pg-scoped.sh ~/Projects/Norma sdcc norma_app_user NORMA_APP_DB_PASSWORD  (LOCAL only, interactive: type new ≥16-char pw twice)"],
+      "routeBack": "handled by the script",
+      "restart": "pm2 restart norma-*",
+      "verify": "node ~/Projects/secrets-manager/cli.js check",
+      "claudeCanRun": []
+    },
+    {
+      "id": "cfg-dw-agents",
+      "title": "DW-Agents ecosystem.config.js → env-first DSN",
+      "type": "config-fix",
+      "severity": "prep",
+      "status": "done",
+      "gates": null,
+      "exposed": "inline DSN wouldn't pick up rotated pw; fixed to process.env.DATABASE_URL || …",
+      "mintUrl": null,
+      "console": [],
+      "routeBack": null,
+      "restart": null,
+      "verify": "committed f9a7548 on rotation-prestage",
+      "claudeCanRun": []
+    },
+    {
+      "id": "cfg-archive-agent",
+      "title": "archive-agent ecosystem.config.js DSN",
+      "type": "config-fix",
+      "severity": "prep",
+      "status": "done",
+      "gates": null,
+      "exposed": "already env-first (no pw literal)",
+      "mintUrl": null,
+      "console": [],
+      "routeBack": null,
+      "restart": null,
+      "verify": "verified env-first",
+      "claudeCanRun": []
+    }
+  ]
+}

← 4fbb01c Add credential rotation checklist (dw_admin pw + 2 Gemini ke  ·  back to Secrets Manager  ·  update tracker: add 'to configure' section for 16 not-set ke f461d5c →