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>
4.0 KiB
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 Vitebase+ FastAPIroot_pathrework across the SPA, the WebSocket, and thembCLI 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 8777publisheshttps://<machine>.<tailnet>.ts.netinstantly.
Hardening already in the app (verify after going live)
Securecookie flag is set when the request arrived via https (readsX-Forwarded-Protothat Caddy sets) — check DevTools showsmb_sessionas 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.0on the tailnet; auth now protects it there too (agents/CLI useMB_TOKEN).
Off-tailnet acceptance test (do this WITH me once Caddy is up)
From a phone on cellular (not on the tailnet):
https://modelbeast.digalot.fyishows the login page over a valid cert.- A guest logs in, runs
flux_local, watches the live log stream, opens the dashboard. MB_HOST=https://modelbeast.digalot.fyi MB_TOKEN=... ./mb opsworks.- Unauthenticated
curl https://modelbeast.digalot.fyi/api/operators→ 401. - A guest gets 403 on
/api/settingsand 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_secretrow 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.