[object Object]

← back to Oauth Proxy Design

initial scaffold (gitify-all 2026-05-06)

fe72b78ead07b13009374706341606762ed16ef1 · 2026-05-06 10:25:44 -0700 · Steve Abrams

Files touched

Diff

commit fe72b78ead07b13009374706341606762ed16ef1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 6 10:25:44 2026 -0700

    initial scaffold (gitify-all 2026-05-06)
---
 .gitignore |  12 ++
 DESIGN.md  | 610 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 622 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e6a9c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+.env
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+*.bak
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..8a77fde
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,610 @@
+# OAuth Proxy Design — Mac2 pm2 Fleet
+**Owner:** theagentabrams@gmail.com  
+**Date:** 2026-05-04  
+**Status:** Design / Not yet implemented
+
+---
+
+## TL;DR
+
+Replace shared Basic Auth (`admin:DWSecure2024!`) across 18+ Mac2 pm2 services with a single Google SSO gate.  
+**Recommended tool: oauth2-proxy** running as a pm2 process on Mac2, fronted by **Caddy** (single binary, no config hell).  
+Traffic path: browser → Caddy (Mac2 tailnet IP, port 443) → oauth2-proxy (port 4180) → backend service (localhost:XXXX).  
+One Google login covers all services. Session stored in a Redis-backed cookie. Audit log written to NDJSON file, queryable by `jq`.  
+Total estimated effort: **12–16 hours** (including Caddy setup, Google Cloud credential registration, rollout of 18 services, and audit wiring).
+
+---
+
+## Problem
+
+| What | Detail |
+|------|--------|
+| Shared secret | `admin:DWSecure2024!` — same credential in every service, every browser, every script |
+| No audit trail | No record of who hit which service when |
+| No revocation granularity | Compromising one service compromises all |
+| No SSO | Each service prompts separately; clearing cookies locks you out of everything |
+| Exposure surface | Tailnet only mitigates network, not credential theft or phishing |
+
+---
+
+## Service Inventory (18 admin/viewer services)
+
+These are the services currently protected by Basic Auth or that expose admin UIs without any auth:
+
+| Service | Port | Notes |
+|---------|------|-------|
+| morning-review | 9762 | Daily findings viewer |
+| video-gallery | 9763 | MP4 session viewer |
+| sf-orchestrator | 9880 | Site Factory control |
+| sf-viewer | 9881 | Site Factory status |
+| sf-admin | (sf-admin) | Site Factory admin gate |
+| ai-factory-orchestrator | 9890 | AI Factory control |
+| ai-factory-viewer | 9891 | AI Factory status |
+| visual-factory-orchestrator | 9892 | Visual Factory control |
+| visual-factory-viewer | 9893 | Visual Factory status |
+| animals-viewer | 9720 | Animals directory viewer |
+| animals-audit | 9721 | Animals audit panel |
+| pd-preview | 9875 | Doctor directory preview |
+| pd-web | 9876 | Doctor directory public |
+| george-gmail | 9850 | Gmail send API |
+| launch-supervisor | 9843 | Launch supervisor |
+| vendor-command-center | 9660 | Vendor admin |
+| debate-ring | (debate-ring) | Debate viewer |
+| yolo-agent | 9670 | YOLO agent UI |
+
+> Note: Services like `norma-*`, `product-agent`, and backend workers (ingest, enrich, dedupe) that are loopback-only and have no browser UI are excluded — they don't need SSO.
+
+---
+
+## Tool Comparison
+
+| | **oauth2-proxy** | **Authelia** | **Pomerium** |
+|---|---|---|---|
+| **Complexity** | Low — single Go binary, ~30 lines of config | Medium — requires its own config YAML + Redis + optional LDAP | Medium-High — Go binary but needs upstream policy DSL; more suited to multi-user orgs |
+| **Google OAuth support** | Native, first-class | Yes, via OIDC provider | Yes, via OIDC |
+| **Session backend** | Cookie (HMAC-signed) or Redis | File / Redis / DB | Redis or in-process |
+| **Audit log** | Request log via stdout/file; `--request-logging` flag | Built-in access.log to file/stdout | Access log to stdout; paid tiers add audit dashboards |
+| **Per-service revocation** | Clear cookie for that upstream only (with path-based routing each service gets its own cookie path) | Session manager UI; revoke individual sessions | Policy-level; session invalidation via API |
+| **Proxy layer needed** | Yes — needs Caddy/nginx in front for TLS + routing | Yes | Yes, or self-terminates TLS |
+| **License** | MIT | Apache 2.0 | BSL (free for self-hosted <3 instances) |
+| **Maturity / stars (2026)** | ~10k GitHub stars, widely deployed | ~22k stars | ~13k stars |
+| **Best for** | Single-owner, all-Google, path/subdomain routing | Multi-user, MFA requirements, enterprise features | Zero-trust, fine-grained policy per route |
+| **Verdict** | **Recommended — minimal ops overhead for solo-owner setup** | Overkill: LDAP/MFA features not needed here | Overkill: policy engine not needed; BSL license risk |
+
+---
+
+## Recommended Architecture
+
+### Components
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│  Your Browser (tailnet-only access)                          │
+│  https://stevestudio2s-mac-studio.tail79cb8e.ts.net/...     │
+└───────────────────────┬─────────────────────────────────────┘
+                        │ HTTPS (Caddy TLS — tailnet cert)
+                        ▼
+┌─────────────────────────────────────────────────────────────┐
+│  Caddy  (Mac2 :443)                                          │
+│  - Terminates TLS via Tailscale's auto-HTTPS (ts.net cert)  │
+│  - Path-based routing: /morning-review/* → :4180            │
+│  - Adds X-Forwarded-For, X-Real-IP headers                  │
+│  - Optionally direct-proxies non-auth services (health/API) │
+└───────────────────────┬─────────────────────────────────────┘
+                        │ HTTP  localhost:4180
+                        ▼
+┌─────────────────────────────────────────────────────────────┐
+│  oauth2-proxy  (Mac2 :4180, pm2 process "oauth-proxy")       │
+│  - Google OIDC: theagentabrams@gmail.com allowed            │
+│  - Signs cookie: _oauth2_proxy (HMAC-SHA256, 24h TTL)       │
+│  - Writes access log: ~/Logs/oauth-proxy/access.ndjson      │
+│  - /oauth2/callback, /oauth2/sign_out, /oauth2/userinfo     │
+│  - Upstream: header X-Forwarded-Uri → route to backend      │
+└───────────────────────┬─────────────────────────────────────┘
+                        │ HTTP  localhost:XXXX (per-service)
+                        ▼
+┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
+│morning-review│  │  sf-viewer   │  │ ai-factory   │  │  george-gmail│
+│  :9762       │  │  :9881       │  │  :9891       │  │  :9850       │
+└──────────────┘  └──────────────┘  └──────────────┘  └──────────────┘
+       ...18 services total, all on localhost, never exposed to tailnet
+```
+
+### Key design decisions
+
+1. **Tailscale auto-HTTPS** — Mac2's tailnet hostname (`stevestudio2s-mac-studio.tail79cb8e.ts.net`) gets a free TLS cert from Tailscale's MagicDNS/Let's Encrypt integration. Caddy uses the `tailscale` TLS provider. Zero cert management.
+
+2. **Caddy, not nginx** — nginx is not installed on Mac2 (confirmed). Caddy is a single `brew install caddy` binary, runs as a pm2 process. No system daemon required. Caddyfile is 40 lines total.
+
+3. **Path-based routing, not subdomain-per-service** — eliminates need for 18 DNS records. All services live under `https://mac2.tail.../morning-review/`, `https://mac2.tail.../sf-viewer/`, etc. Cookie scope is `/` so one login covers all paths.
+
+4. **oauth2-proxy as pm2 process** — consistent with how everything else on Mac2 is managed. `pm2 start oauth2-proxy -- --config /Users/stevestudio2/Projects/oauth-proxy/config.cfg`.
+
+5. **Allowed email whitelist** — `--email-domain=` is too broad. Use `--authenticated-emails-file` pointing to a file containing `theagentabrams@gmail.com`. Adding a second person = one line added.
+
+6. **Per-service cookie path** — to enable per-service revocation, oauth2-proxy is run in **upstream routing mode**: each service gets a distinct `--upstream` entry. For revocation of a single service, set a server-side blocklist entry (see audit log section) or use `--cookie-path=/morning-review/` per instance. For simplicity, Phase 1 uses a single shared session; Phase 2 can fork into per-service oauth2-proxy instances.
+
+---
+
+## Audit Log
+
+oauth2-proxy emits a structured access log per request when `--request-logging=true` and `--request-logging-format` is set to JSON. Pipe stdout to a file via pm2's log path.
+
+Log entries look like:
+```json
+{"level":"info","ts":"2026-05-04T09:15:33Z","logger":"oauthproxy","msg":"GET /morning-review/ 200","remote_addr":"100.65.187.120","user":"theagentabrams@gmail.com","upstream":"morning-review","latency_ms":3}
+```
+
+Query examples:
+```bash
+# All requests from last hour
+jq 'select(.ts > "2026-05-04T08:")' ~/Logs/oauth-proxy/access.ndjson
+
+# Which services accessed today
+jq -r .upstream ~/Logs/oauth-proxy/access.ndjson | sort | uniq -c | sort -rn
+
+# 401/403 rejections (non-owner attempts)
+jq 'select(.msg | contains("401") or contains("403"))' ~/Logs/oauth-proxy/access.ndjson
+```
+
+Log rotation: `logrotate` config or a nightly launchd job (`find ~/Logs/oauth-proxy -mtime +30 -delete`).
+
+---
+
+## Per-Service Revocation
+
+**Phase 1 (single proxy instance):**  
+oauth2-proxy stores session state in a signed cookie. Signing key is `--cookie-secret`. Rotating the secret invalidates ALL sessions (all services). This is the "nuke everything" approach — fine for a solo operator. Takes 30 seconds.
+
+**Phase 2 (per-service instances, optional):**  
+Run one oauth2-proxy per service, each on its own port (e.g., 4181=morning-review, 4182=sf-viewer). Caddy routes to the matching proxy. Each proxy has an independent cookie secret. Revoking service X = `pm2 restart oauth-proxy-morning-review` with a new `COOKIE_SECRET` env var. The other 17 sessions are unaffected.
+
+This is a configuration multiplier (18 plist entries), not a code change. The Phase 2 migration script is a `for` loop over the service map.
+
+---
+
+## Network Perimeter
+
+**Recommendation: Tailnet-only.**  
+Bind Caddy to the Tailscale interface IP only (`100.65.187.120:443`), not `0.0.0.0:443`. This means:
+- LAN access (192.168.x.x) does NOT reach the proxy — must be on Tailscale
+- Airplane mode / no network = no access (see drawbacks)
+- Kamatera and other tailnet nodes reach Mac2 services only if their Tailscale ACL permits it
+
+To allow LAN as a fallback (e.g., office desk access without Tailscale):  
+Bind Caddy additionally to `192.168.1.X:443` with a self-signed cert. Keep the tailnet binding as primary. This is optional and noted in the Caddyfile below.
+
+---
+
+## Configuration Artifacts
+
+### Google Cloud OAuth setup (one-time)
+
+1. Go to console.cloud.google.com → existing project (or create `dw-agents-sso`)
+2. APIs & Services → Credentials → Create OAuth Client ID
+3. Application type: **Web application**
+4. Authorized redirect URIs:
+   - `https://stevestudio2s-mac-studio.tail79cb8e.ts.net/oauth2/callback`
+5. Save CLIENT_ID and CLIENT_SECRET to secrets manager:
+   ```bash
+   node ~/Projects/secrets-manager/cli.js add OAUTH_PROXY_CLIENT_ID "xxxx.apps.googleusercontent.com"
+   node ~/Projects/secrets-manager/cli.js add OAUTH_PROXY_CLIENT_SECRET "GOCSPX-xxxx"
+   ```
+
+### oauth2-proxy config file
+
+Path: `/Users/stevestudio2/Projects/oauth-proxy/config.cfg`
+
+```ini
+# oauth2-proxy config — Mac2 DW-Agents fleet
+# https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview
+
+provider = "google"
+client-id = "${OAUTH_PROXY_CLIENT_ID}"
+client-secret = "${OAUTH_PROXY_CLIENT_SECRET}"
+redirect-url = "https://stevestudio2s-mac-studio.tail79cb8e.ts.net/oauth2/callback"
+
+# Allowlist — single file, one email per line
+authenticated-emails-file = "/Users/stevestudio2/Projects/oauth-proxy/allowed-emails.txt"
+
+# Cookie settings
+cookie-secret = "${OAUTH_PROXY_COOKIE_SECRET}"  # generate: openssl rand -base64 32
+cookie-secure = true
+cookie-httponly = true
+cookie-samesite = "lax"
+cookie-expire = "24h"
+cookie-refresh = "1h"
+cookie-name = "_dw_sso"
+
+# Upstream: path-based, Caddy handles the actual routing
+# oauth2-proxy receives the full URL from Caddy via X-Forwarded-Uri
+# and validates auth; on success passes to upstream via Caddy's proxy
+# (run oauth2-proxy in "reverse proxy" mode, NOT standalone upstream mode)
+# Caddy → oauth2-proxy auth check → Caddy → backend
+http-address = "127.0.0.1:4180"
+
+# Logging
+request-logging = true
+request-logging-format = '{"ts":"{{.Timestamp}}","user":"{{.Username}}","method":"{{.Method}}","path":"{{.Path}}","status":{{.StatusCode}},"upstream":"{{.Upstream}}","latency_ms":{{.RequestDuration}}}'
+standard-logging = true
+
+# Session
+session-store-type = "cookie"
+
+# Reverse proxy mode — do not rewrite upstream URLs
+skip-provider-button = true
+```
+
+`/Users/stevestudio2/Projects/oauth-proxy/allowed-emails.txt`:
+```
+theagentabrams@gmail.com
+```
+
+### Caddyfile
+
+Path: `/Users/stevestudio2/Projects/oauth-proxy/Caddyfile`
+
+```caddyfile
+# Bind to Tailscale IP only for tailnet-only access
+{
+    # Global options
+    admin off
+    log {
+        output file /Users/stevestudio2/Logs/caddy/access.log
+        format json
+    }
+}
+
+# Tailnet hostname — Caddy uses Tailscale TLS automatically
+stevestudio2s-mac-studio.tail79cb8e.ts.net {
+    tls {
+        # Tailscale provisions cert automatically via its ACME endpoint
+        # If using tailscale serve instead, omit this block
+    }
+
+    # oauth2-proxy auth subrequest for all admin routes
+    # Pattern: all paths except /oauth2/* are auth-gated
+    @needs_auth not path /oauth2/*
+    forward_auth @needs_auth localhost:4180 {
+        uri /oauth2/auth
+        copy_headers X-Auth-Request-User X-Auth-Request-Email X-Auth-Request-Access-Token
+    }
+
+    # OAuth2-proxy callback and sign-out (unprotected)
+    handle /oauth2/* {
+        reverse_proxy localhost:4180
+    }
+
+    # ── Service routes ─────────────────────────────────────────────
+    handle /morning-review/* {
+        uri strip_prefix /morning-review
+        reverse_proxy localhost:9762
+    }
+
+    handle /video-gallery/* {
+        uri strip_prefix /video-gallery
+        reverse_proxy localhost:9763
+    }
+
+    handle /sf-orchestrator/* {
+        uri strip_prefix /sf-orchestrator
+        reverse_proxy localhost:9880
+    }
+
+    handle /sf-viewer/* {
+        uri strip_prefix /sf-viewer
+        reverse_proxy localhost:9881
+    }
+
+    handle /ai-factory-viewer/* {
+        uri strip_prefix /ai-factory-viewer
+        reverse_proxy localhost:9891
+    }
+
+    handle /ai-factory/* {
+        uri strip_prefix /ai-factory
+        reverse_proxy localhost:9890
+    }
+
+    handle /visual-factory-viewer/* {
+        uri strip_prefix /visual-factory-viewer
+        reverse_proxy localhost:9893
+    }
+
+    handle /visual-factory/* {
+        uri strip_prefix /visual-factory
+        reverse_proxy localhost:9892
+    }
+
+    handle /animals-viewer/* {
+        uri strip_prefix /animals-viewer
+        reverse_proxy localhost:9720
+    }
+
+    handle /animals-audit/* {
+        uri strip_prefix /animals-audit
+        reverse_proxy localhost:9721
+    }
+
+    handle /pd-preview/* {
+        uri strip_prefix /pd-preview
+        reverse_proxy localhost:9875
+    }
+
+    handle /pd-web/* {
+        uri strip_prefix /pd-web
+        reverse_proxy localhost:9876
+    }
+
+    handle /george-gmail/* {
+        uri strip_prefix /george-gmail
+        reverse_proxy localhost:9850
+    }
+
+    handle /launch-supervisor/* {
+        uri strip_prefix /launch-supervisor
+        reverse_proxy localhost:9843
+    }
+
+    handle /vendor-command-center/* {
+        uri strip_prefix /vendor-command-center
+        reverse_proxy localhost:9660
+    }
+
+    handle /debate-ring/* {
+        uri strip_prefix /debate-ring
+        reverse_proxy localhost:9075
+    }
+
+    handle /yolo-agent/* {
+        uri strip_prefix /yolo-agent
+        reverse_proxy localhost:9670
+    }
+
+    # Dashboard — shows all service links
+    handle / {
+        respond `
+<html><head><title>DW-Agents Hub</title></head><body>
+<h2>DW-Agents — Mac2 Services</h2>
+<ul>
+  <li><a href="/morning-review/">Morning Review</a></li>
+  <li><a href="/video-gallery/">Video Gallery</a></li>
+  <li><a href="/sf-viewer/">Site Factory Viewer</a></li>
+  <li><a href="/sf-orchestrator/">Site Factory Orchestrator</a></li>
+  <li><a href="/ai-factory-viewer/">AI Factory Viewer</a></li>
+  <li><a href="/visual-factory-viewer/">Visual Factory Viewer</a></li>
+  <li><a href="/animals-viewer/">Animals Viewer</a></li>
+  <li><a href="/pd-preview/">Professional Directory Preview</a></li>
+  <li><a href="/george-gmail/">George Gmail</a></li>
+  <li><a href="/yolo-agent/">YOLO Agent</a></li>
+</ul>
+<p><a href="/oauth2/sign_out">Sign out</a></p>
+</body></html>` 200
+    }
+}
+```
+
+### pm2 ecosystem config additions
+
+Append to `/Users/stevestudio2/Projects/oauth-proxy/ecosystem.config.js` (then `pm2 start ecosystem.config.js`):
+
+```javascript
+module.exports = {
+  apps: [
+    {
+      name: 'caddy-proxy',
+      script: 'caddy',
+      args: 'run --config /Users/stevestudio2/Projects/oauth-proxy/Caddyfile --adapter caddyfile',
+      // caddy is not a Node script — use interpreter: none trick
+      interpreter: 'none',
+      env: {},
+      out_file: '/Users/stevestudio2/Logs/caddy/out.log',
+      error_file: '/Users/stevestudio2/Logs/caddy/err.log',
+      autorestart: true,
+      watch: false,
+    },
+    {
+      name: 'oauth-proxy',
+      script: '/opt/homebrew/bin/oauth2-proxy',
+      args: '--config /Users/stevestudio2/Projects/oauth-proxy/config.cfg',
+      interpreter: 'none',
+      env_file: '/Users/stevestudio2/Projects/oauth-proxy/.env',
+      out_file: '/Users/stevestudio2/Logs/oauth-proxy/out.log',
+      error_file: '/Users/stevestudio2/Logs/oauth-proxy/err.log',
+      autorestart: true,
+      watch: false,
+    }
+  ]
+};
+```
+
+`/Users/stevestudio2/Projects/oauth-proxy/.env` (do NOT commit; route via secrets-manager):
+```
+OAUTH_PROXY_CLIENT_ID=xxxx.apps.googleusercontent.com
+OAUTH_PROXY_CLIENT_SECRET=GOCSPX-xxxx
+OAUTH_PROXY_COOKIE_SECRET=<openssl rand -base64 32>
+```
+
+### launchd keepalive plist (alternative to pm2 for the proxy pair)
+
+If you prefer launchd over pm2 for the proxy (since pm2 is already managing 60+ processes), here is the Caddy plist. Mirror for oauth2-proxy by substituting the program args.
+
+`~/Library/LaunchAgents/com.steve.caddy-proxy.plist`:
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
+  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>Label</key>         <string>com.steve.caddy-proxy</string>
+  <key>ProgramArguments</key>
+  <array>
+    <string>/opt/homebrew/bin/caddy</string>
+    <string>run</string>
+    <string>--config</string>
+    <string>/Users/stevestudio2/Projects/oauth-proxy/Caddyfile</string>
+    <string>--adapter</string>
+    <string>caddyfile</string>
+  </array>
+  <key>EnvironmentVariables</key>
+  <dict>
+    <key>HOME</key> <string>/Users/stevestudio2</string>
+  </dict>
+  <key>RunAtLoad</key>  <true/>
+  <key>KeepAlive</key>  <true/>
+  <key>StandardOutPath</key>  <string>/Users/stevestudio2/Logs/caddy/out.log</string>
+  <key>StandardErrorPath</key> <string>/Users/stevestudio2/Logs/caddy/err.log</string>
+</dict>
+</plist>
+```
+
+Load with: `launchctl load ~/Library/LaunchAgents/com.steve.caddy-proxy.plist`
+
+---
+
+## Integration with Existing google-oauth-integration Skill
+
+The existing skill at `~/.claude/skills/google-oauth-integration/` uses **in-process auth middleware** (`requireGlobalAuth` in `shared-global-auth.ts`) and a custom Express auth server on port 9999. That is a different pattern from the external reverse-proxy approach designed here.
+
+**These can coexist** without conflict:
+- The existing skill handles agents that import `requireGlobalAuth` (typically Kamatera/DW-Agents on the remote server)
+- The new oauth2-proxy design handles Mac2 local services that do not import the middleware
+
+**Migration path if you want a single auth system long-term:**  
+Point the existing `AUTH_SERVER` constant to the oauth2-proxy forward-auth endpoint instead of the custom Express server. That is a Phase 3 item and not required for initial rollout.
+
+**Re-use the same Google Cloud OAuth client ID** — simply add the Mac2 tailnet callback URL as an additional Authorized Redirect URI in the existing Google Cloud project.
+
+---
+
+## Phased Rollout Plan
+
+### Week 1 — Infrastructure (4 hours)
+
+- [ ] `brew install caddy oauth2-proxy` on Mac2
+- [ ] Create Google OAuth credentials (add redirect URI to existing project or new one)
+- [ ] Generate `COOKIE_SECRET`: `openssl rand -base64 32`
+- [ ] Route all three secrets via `secrets-manager cli.js add`
+- [ ] Create `~/Projects/oauth-proxy/` directory structure
+- [ ] Write `config.cfg`, `Caddyfile`, `allowed-emails.txt`
+- [ ] Start both processes via pm2 (not yet fronting any production service)
+- [ ] Test: `curl http://localhost:4180/oauth2/auth` should return 401
+- [ ] Test: browser to `https://stevestudio2s-mac-studio.tail79cb8e.ts.net/` should show Google login
+
+### Week 2 — Pilot: 2 services (3 hours)
+
+Pilot with `morning-review` (9762) and `video-gallery` (9763) — both new, no existing Basic Auth dependency.
+
+- [ ] Add Caddyfile routes for `/morning-review/` and `/video-gallery/`
+- [ ] Verify SSO login, session persistence across browser restart
+- [ ] Verify audit log entries appear in `~/Logs/oauth-proxy/access.ndjson`
+- [ ] Verify old direct-port access `localhost:9762` still works from localhost (curl only, not exposed on tailnet)
+- [ ] Remove Basic Auth middleware from `morning-review/server.js` and `video-gallery/server.js`
+
+### Week 3 — Rolling migration: remaining 16 services (4 hours)
+
+Group by project family to batch test:
+
+1. Site Factory: sf-orchestrator, sf-viewer, sf-admin
+2. AI/Visual Factory: ai-factory-*, visual-factory-*
+3. Directory services: animals-viewer, animals-audit, pd-preview, pd-web
+4. Tooling: george-gmail, launch-supervisor, vendor-command-center, debate-ring, yolo-agent
+
+For each group:
+- [ ] Add Caddyfile handle block
+- [ ] `caddy reload --config /Users/stevestudio2/Projects/oauth-proxy/Caddyfile`
+- [ ] Test new URL
+- [ ] Comment out / remove `basicAuth()` middleware in the service's `server.js`
+- [ ] `pm2 restart <service>`
+
+### Week 4 — Hardening (2 hours)
+
+- [ ] Add log rotation launchd job (`com.steve.oauth-proxy-logrotate`)
+- [ ] Add Caddy + oauth-proxy to `pm2 save` / `pm2 startup` output
+- [ ] Write a 5-line audit query alias to `~/.zshrc`:
+  ```bash
+  alias dw-audit="jq -r '[.ts,.user,.path,.status] | @tsv' ~/Logs/oauth-proxy/access.ndjson | tail -50"
+  ```
+- [ ] Update MEMORY.md with new service URLs
+- [ ] Optionally: Phase 2 per-service cookie isolation (fork to 18 oauth2-proxy instances)
+
+---
+
+## Drawbacks and Mitigations
+
+| Drawback | Severity | Mitigation |
+|----------|----------|-----------|
+| Extra hop latency (~2ms) | Low | Caddy+oauth2-proxy add <5ms on localhost; imperceptible |
+| Google session expiry (24h) | Low | `--cookie-refresh=1h` silently refreshes while browser is active; only trips on >24h away |
+| Airplane mode / no internet | Medium | Google token validation requires internet contact. Mitigation: run services directly via `localhost:PORT` when offline (they are still accessible locally; just not via the SSO proxy URL) |
+| Proxy is single point of failure | Medium | If Caddy or oauth2-proxy crashes, ALL services lose their SSO URL. Mitigation: launchd `KeepAlive` + `pm2 autorestart`; monitor with `com.steve.pm2-resurrect` |
+| macOS Gatekeeper on oauth2-proxy binary | Low | `brew install oauth2-proxy` bypasses Gatekeeper; do not download the binary directly |
+| Google Cloud project dependency | Low | If the OAuth client is deleted or suspended, all auth breaks. Keep client ID in secrets manager with a backup note |
+| Cookie secret rotation nukes all sessions | Low | Document the rotation procedure; only matters if Steve has multiple active browser sessions |
+
+---
+
+## When NOT to Use This
+
+**Tailscale ACLs alone are sufficient when:**
+- The service is a background worker with no browser UI (norma-*, ingest, enrich, dedupe pipelines)
+- The service is called only machine-to-machine (george-gmail called from scripts, not browser)
+- The service already blocks all non-localhost requests at the Express level
+- You are prototyping and will tear it down within a week
+
+**Add the OAuth proxy when:**
+- The service has a human-facing browser UI
+- You want an audit trail of who clicked what
+- The service will eventually be shared with a second person
+- The service exposes any write action (POST /api/send, POST /api/launch, etc.)
+
+**Pure Tailscale ACL approach (no oauth2-proxy, no Caddy):**  
+Enable Tailscale's built-in `tailscale serve` and lock the ACL to `src == "steve@"` only. This works for read-only dashboards where even a compromised Tailscale device grants full access. Zero additional infrastructure. Appropriate for viewer-only services (video-gallery, sf-viewer) if you decide the audit log is not a requirement.
+
+---
+
+## Estimated Effort
+
+| Phase | Task | Hours |
+|-------|------|-------|
+| Week 1 | Infrastructure: brew install, Google creds, pm2 start | 4h |
+| Week 2 | Pilot: 2 services, test, fix path-stripping edge cases | 3h |
+| Week 3 | Rolling migration: 16 services (15 min each) | 4h |
+| Week 4 | Log rotation, pm2 persistence, audit alias, docs | 2h |
+| **Total** | | **13h** |
+
+Stretch goal — Phase 2 per-service instances: +4h (scripted, not manual).
+
+---
+
+## File Structure
+
+```
+~/Projects/oauth-proxy/
+├── config.cfg              # oauth2-proxy config (env var references for secrets)
+├── Caddyfile               # Caddy reverse proxy config
+├── allowed-emails.txt      # Allowlist: one email per line
+├── ecosystem.config.js     # pm2 process definitions for caddy + oauth2-proxy
+└── .env                    # Secrets (NOT committed; managed by secrets-manager)
+
+~/Logs/
+├── caddy/
+│   ├── access.log          # Caddy access log (JSON)
+│   ├── out.log
+│   └── err.log
+└── oauth-proxy/
+    ├── access.ndjson       # oauth2-proxy audit log
+    ├── out.log
+    └── err.log
+```
+
+---
+
+## References
+
+- oauth2-proxy docs: https://oauth2-proxy.github.io/oauth2-proxy/
+- Caddy reverse_proxy docs: https://caddyserver.com/docs/caddyfile/directives/reverse_proxy
+- Caddy forward_auth docs: https://caddyserver.com/docs/caddyfile/directives/forward_auth
+- Tailscale HTTPS certs for Caddy: https://tailscale.com/kb/1181/caddy
+- Existing DW-Agents auth skill: `~/.claude/skills/google-oauth-integration/SKILL.md`
+- Existing auth server: `/Users/stevestudio2/kamatera-mirror/DW-Agents/global-auth-system/auth-server.ts`

(oldest)  ·  back to Oauth Proxy Design  ·  (newest)