modelbeast/docs/VPS.md
MODELBEAST 514ec6cdcc Security hardening from adversarial review (5-agent workflow)
Fixed confirmed findings before public exposure:
HIGH:
- upload filename path traversal → store.safe_name() strips to basename
- login rate-limit XFF bypass → key on request.client.host + per-username bucket;
  auth.check_login() burns bcrypt time on unknown users (no enumeration)
- cross-user read access → per-user isolation: guests see/use/download/delete only
  their own assets & jobs (owner sees all); WS job events scoped per-user
MEDIUM:
- unbounded upload read → bounded chunked streaming to the 1GB cap
- asset member path check → Path.is_relative_to boundary + ownership gate
- WS token-in-query-string leak → session-cookie-only WS auth
LOW:
- retry_job bypassed the per-user job cap → cap now checked on retry
- wholesale API-key injection → env_for_operator injects a paid key only to
  operators that declare it (guest local jobs never receive fal/OpenRouter keys)
- session revocation → users.session_epoch, bumped on password change
- int() 500s → 400; net-lane defense-in-depth (guests blocked by requires_env AND
  resources==net, so a mis-tagged paid op is still blocked)
+ public /api/health for serve.sh & proxy; docs/VPS.md; mb MB_TOKEN bearer auth

tests/smoke.sh: 34 checks passing incl. all new hardening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 12:25:13 +10:00

4.0 KiB

Public hosting — MODELBEAST at modelbeast.digalot.fyi

Expose MODELBEAST to the internet without exposing the M3 Ultra directly: a spare VPS is the only public surface and reverse-proxies to the Mac over the tailnet. If the VPS is ever compromised you just remove one tailnet node; the Mac is never on the public internet.

  friend's browser ──https──▶ VPS (Caddy, public)
                                  │  reverse_proxy over tailnet
                                  ▼
                          M3 Ultra 100.89.131.57:8777  (auth enforced here)

Prerequisite: auth must be live on the Mac first (HANDOFF2 Phase 1 — done). Do not open this to the internet until mb/the UI require a login. Every public request now hits the login gate; guests are local-only by hard server-side rule.

On the VPS (run these — you own that box; I can't reach it)

# 1) join the same tailnet as the M3 Ultra
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

# 2) Caddy (automatic HTTPS via Let's Encrypt)
sudo apt update && sudo apt install -y caddy

Write /etc/caddy/Caddyfile:

modelbeast.digalot.fyi {
    encode zstd gzip
    request_body {
        max_size 1GB          # matches the app's upload cap
    }
    reverse_proxy 100.89.131.57:8777
}

# optional second domain, same backend:
# modelbeast.partyl.party {
#     encode zstd gzip
#     request_body { max_size 1GB }
#     reverse_proxy 100.89.131.57:8777
# }
sudo systemctl reload caddy

DNS: add an A record modelbeast → the VPS's public IP on digalot.fyi (and partyl.party if using it). Caddy fetches the TLS cert automatically on first request.

Why these choices

  • Subdomain, not digalot.fyi/modelbeast. A path prefix would force a Vite base + FastAPI root_path rework across the SPA, the WebSocket, and the mb CLI for zero benefit. modelbeast.<domain> needs no app changes.
  • Caddy proxies WebSockets automatically — the live job-log stream works through it. The session cookie is same-origin (browser talks to the subdomain for both HTTP and WS), so WS auth via cookie just works.
  • Auth lives in the app, not the proxy. Proxy basic-auth can't do per-user roles, the guest local-only rule, or per-user job caps. All of that is enforced in FastAPI.
  • Rejected: Tailscale Funnel. Zero-VPS and simplest, but no custom domain and it's bandwidth-capped. Keep it in your back pocket as the fallback if the VPS dies: tailscale funnel 8777 publishes https://<machine>.<tailnet>.ts.net instantly.

Hardening already in the app (verify after going live)

  • Secure cookie flag is set when the request arrived via https (reads X-Forwarded-Proto that Caddy sets) — check DevTools shows mb_session as Secure.
  • Upload cap 1 GB in the app and in Caddy (request_body max_size).
  • Login rate-limit (5/min) keyed on X-Forwarded-For.
  • The Mac keeps binding 0.0.0.0 on the tailnet; auth now protects it there too (agents/CLI use MB_TOKEN).

Off-tailnet acceptance test (do this WITH me once Caddy is up)

From a phone on cellular (not on the tailnet):

  1. https://modelbeast.digalot.fyi shows the login page over a valid cert.
  2. A guest logs in, runs flux_local, watches the live log stream, opens the dashboard.
  3. MB_HOST=https://modelbeast.digalot.fyi MB_TOKEN=... ./mb ops works.
  4. Unauthenticated curl https://modelbeast.digalot.fyi/api/operators → 401.
  5. A guest gets 403 on /api/settings and on a direct cloud-operator job POST.

Runbook

  • Server stays a single uvicorn worker (the in-process job queue + shared SQLite connection require exactly one worker — do not add --workers).
  • Restart the Mac server: ./scripts/serve.sh. Caddy needs no restart for that.
  • Rotate a leaked session-signing key: delete the auth_secret row from the settings table (sqlite3 data/modelbeast.db "DELETE FROM settings WHERE key='auth_secret'") and restart — a new one is generated and all existing sessions are invalidated.