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>
94 lines
4.0 KiB
Markdown
94 lines
4.0 KiB
Markdown
# 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.<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.
|