← back to Tailscale Funnel Research
initial scaffold (gitify-all 2026-05-06)
05e252a6a635852a9dc452cc3578b0250adcf4ed · 2026-05-06 10:25:50 -0700 · Steve Abrams
Files touched
A .gitignoreA REPORT.mdA migrate-fleet.sh
Diff
commit 05e252a6a635852a9dc452cc3578b0250adcf4ed
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 10:25:50 2026 -0700
initial scaffold (gitify-all 2026-05-06)
---
.gitignore | 12 +++++++
REPORT.md | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
migrate-fleet.sh | 66 ++++++++++++++++++++++++++++++++++
3 files changed, 184 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/REPORT.md b/REPORT.md
new file mode 100644
index 0000000..de40fa0
--- /dev/null
+++ b/REPORT.md
@@ -0,0 +1,106 @@
+# Tailscale Serve / Funnel — Mac2 pm2 Fleet Feasibility Report
+
+**Date:** 2026-05-05
+**Mac Studio 2:** tailnet IP 100.65.187.120, Tailscale Standalone variant (`io.tailscale.ipn.macsys`)
+
+## TL;DR
+
+- **Tailscale Serve is viable and recommended** — replaces Basic Auth on the 18 tailnet-gated services. Each service: `tailscale serve --bg --https=<port> http://localhost:<port>`; flip 0.0.0.0 → 127.0.0.1.
+- **Basic Auth middleware can be stripped entirely.** Serve gates every request behind tailnet membership and injects `Tailscale-User-Login` headers automatically.
+- **One hard one-time blocker:** Enable HTTPS Certificates at https://login.tailscale.com/admin/dns first.
+
+## Serve vs. Funnel — do not confuse
+
+| | Tailscale Serve | Tailscale Funnel |
+|---|---|---|
+| Who can access? | Tailnet members only | Public internet |
+| HTTPS auto-cert | Yes | Yes |
+| Identity headers | Yes (`Tailscale-User-Login`) | No |
+| Port restrictions | Arbitrary | 443/8443/10000 only |
+| What we want? | YES | NO |
+
+## Architecture: Before vs. After
+
+### Current (0.0.0.0 + Basic Auth)
+- 18 services bound 0.0.0.0, gated by `admin:DWSecure2024!` (in 41+ files)
+- Reachable from LAN (192.168.1.x); only Basic Auth blocks
+- Rotation = edit 41 files + fleet restart
+
+### Proposed (127.0.0.1 + Tailscale Serve)
+- 18 services bound 127.0.0.1 (LAN-dark)
+- HTTPS via auto LE cert: `https://stevestudio2s-mac-studio.<tailnet>.ts.net:<port>`
+- Tailnet membership IS the auth — no shared password
+- Identity headers injected by Serve
+
+## Migration commands
+
+```bash
+# ── ONE-TIME PREREQ ──
+# 1. Enable HTTPS at https://login.tailscale.com/admin/dns
+# 2. Provision cert:
+TAILNET=$(tailscale status --json | python3 -c "import sys,json; print(json.load(sys.stdin)['MagicDNSSuffix'])")
+tailscale cert "stevestudio2s-mac-studio.${TAILNET}"
+
+# ── REGISTER SERVICES (services stay live during this step) ──
+for port in 9762 9763 9706 9707 9871 9873 9875 9890 9891 9892; do
+ tailscale serve --bg --https=$port http://localhost:$port
+done
+for port in 9720 9721 9722 9723 9648 9881 9882 9725; do
+ tailscale serve --bg --https=$port http://localhost:$port
+done
+tailscale serve status # confirm
+
+# ── PER-SERVICE FLIP (do one at a time) ──
+# 1. Edit server.js: '0.0.0.0' → '127.0.0.1', remove Basic Auth middleware
+# 2. pm2 restart <svc> --update-env
+# 3. curl https://stevestudio2s-mac-studio.${TAILNET}:<port>/healthz
+# 4. pm2 save
+
+# ── ROLLBACK (any service, any time) ──
+tailscale serve --https=<port> off
+# revert code, pm2 restart, done
+```
+
+## Excluded (do not migrate)
+
+| Service | Port | Reason |
+|---|---|---|
+| pd-preview | 9875 | Public Cloudflare tunnel target |
+| lawyer-directory-builder | 9701 | Own session/passport auth |
+| pd-api | 9874 | loopbackSyntheticAdmin design |
+| Claude IDE bridges | 18789/91/92 | Hard rule: never touch |
+
+## Side-by-side
+
+| Dimension | Current | Proposed |
+|---|---|---|
+| Auth | DWSecure2024! shared password | Tailnet membership |
+| Credential count | 1 password in 41+ files | Zero |
+| LAN exposure | Yes | No (127.0.0.1) |
+| HTTPS | No | Auto LE cert |
+| Migration effort | N/A | ~2h for 18 services |
+| Reversibility | N/A | 30 sec per-service rollback |
+
+## Risks
+
+| Risk | Severity | Mitigation |
+|---|---|---|
+| Tailscale daemon stops → all services unreachable from MBP | Medium | Existing pm2-hawk + launchd auto-restart |
+| HTTPS prereq missing → mass outage | Medium | Do prereq first, verify cert |
+| pd-preview public Cloudflare tunnel | High | Excluded |
+| Wave-1 dead-code DWSecure2024! sprawl | Medium | Post-migration cleanup sprint |
+| pm2 restart 1-3s 502 from Serve | Low | Acceptable for internal tools |
+
+## Final Recommendation
+
+**Switch.** Migration eliminates DWSecure2024! sprawl at the root. ~2 hours fleet-wide; ~30 sec rollback per service. The 41 hardcoded credential instances become dead code to scrub at leisure.
+
+**Hard prerequisite:** enable HTTPS Certificates in Tailscale admin DNS page before any `tailscale serve --https` command — otherwise zero progress.
+
+## Sources
+
+- https://tailscale.com/docs/features/tailscale-serve
+- https://tailscale.com/docs/reference/tailscale-cli/serve
+- https://tailscale.com/docs/features/tailscale-funnel
+- https://tailscale.com/docs/how-to/set-up-https-certificates
+- https://tailscale.com/blog/reintroducing-serve-funnel
diff --git a/migrate-fleet.sh b/migrate-fleet.sh
new file mode 100755
index 0000000..41c361b
--- /dev/null
+++ b/migrate-fleet.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+# Tailscale Serve fleet migration — register + smoke + report.
+# RUN ONLY AFTER enabling HTTPS Certificates at https://login.tailscale.com/admin/dns
+#
+# Phase 1 (this script): register all 18 services with `tailscale serve --bg --https`
+# services stay live on 0.0.0.0 + Basic Auth during this step
+# Phase 2 (manual, per service, your call):
+# - edit server.js: '0.0.0.0' → '127.0.0.1', remove Basic Auth middleware
+# - pm2 restart <svc>
+# - verify https URL still works
+# - pm2 save
+#
+# Rollback any service: `tailscale serve --https=<port> off`
+
+set -uo pipefail
+
+FQDN="stevestudio2s-mac-studio.tail79cb8e.ts.net"
+
+echo "=== prerequisite check ==="
+mkdir -p /tmp/tscerts.test && cd /tmp/tscerts.test
+tailscale cert "$FQDN" > /tmp/tscert.log 2>&1 &
+P=$!
+for i in $(seq 1 12); do sleep 1; ! kill -0 $P 2>/dev/null && break; done
+kill -9 $P 2>/dev/null
+if [ ! -s "$FQDN.crt" ]; then
+ echo "FATAL: HTTPS cert NOT provisioned. Visit https://login.tailscale.com/admin/dns and enable HTTPS Certificates."
+ cat /tmp/tscert.log 2>/dev/null
+ rm -rf /tmp/tscerts.test /tmp/tscert.log
+ exit 1
+fi
+echo " ✓ cert exists ($(wc -c < $FQDN.crt) bytes)"
+rm -rf /tmp/tscerts.test /tmp/tscert.log
+cd ~
+
+echo ""
+echo "=== registering 18 services with tailscale serve --bg --https ==="
+declare -a PORTS=(9762 9763 9706 9707 9871 9873 9874 9875 9890 9891 9892 9720 9721 9722 9723 9648 9881 9882 9725)
+for port in "${PORTS[@]}"; do
+ out=$(tailscale serve --bg --https=$port http://localhost:$port 2>&1)
+ if echo "$out" | grep -q "Available within your tailnet\|Started serving\|already exists\|configured"; then
+ echo " ✓ $port"
+ elif echo "$out" | grep -q "error"; then
+ echo " ✗ $port: $(echo "$out" | head -1)"
+ else
+ echo " ? $port: $(echo "$out" | head -1)"
+ fi
+done
+
+echo ""
+echo "=== tailscale serve status ==="
+tailscale serve status
+
+echo ""
+echo "=== smoke test (HTTP via Serve, services still on 0.0.0.0+BasicAuth at this stage) ==="
+for port in 9762 9763 9881; do
+ code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 -u admin:DWSecure2024! "https://${FQDN}:${port}/healthz" 2>/dev/null || echo "ERR")
+ printf " https://%s:%s/healthz → %s\n" "$FQDN" "$port" "$code"
+done
+
+echo ""
+echo "DONE. Services are now reachable BOTH via:"
+echo " • Old: http://100.65.187.120:<port>/ (Basic Auth) ← still works"
+echo " • New: https://${FQDN}:<port>/ (tailnet-gated, no auth)"
+echo ""
+echo "Phase 2: per-service code edit (0.0.0.0 → 127.0.0.1, remove BasicAuth middleware)"
+echo "Rollback any service: tailscale serve --https=<port> off"
(oldest)
·
back to Tailscale Funnel Research
·
(newest)