← back to Oauth Proxy Design

DESIGN.md

611 lines

# 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`