# 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) ```bash # 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 # } ``` ```bash 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.` 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://..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.