feat(webui): auth and deployment boundary (#435)

Add bind-host assessment that refuses 0.0.0.0/:: without override,
warns on non-loopback binds, and documents internal-only MVP serving.
Health endpoint exposes deployment metadata; docs cover Access/VPN/WARP
and runtime env assumptions without embedding secrets in the client.

Closes #435
This commit is contained in:
2026-07-09 11:50:40 -04:00
parent 8d1098f916
commit 975674d7f5
7 changed files with 362 additions and 3 deletions
+59
View File
@@ -0,0 +1,59 @@
# Web UI deployment boundary (#435)
The MCP Control Plane web UI is an **internal operator console**, not a
customer-facing application. The MVP assumes local or trusted-network access
only.
## MVP deployment model
- **Default bind:** `127.0.0.1:8765` (`WEBUI_HOST` / `WEBUI_PORT`)
- **Authentication:** none in MVP — protection comes from network placement
- **Mutations:** read-only routes; gated write actions remain disabled (#434)
- **Secrets:** resolved server-side via `gitea_auth` / `GITEA_MCP_CONFIG`; never
embedded in HTML, JavaScript, or browser storage
Do **not** expose the UI on the public internet without an access layer.
## Beyond localhost
If the UI must be reachable outside the operator laptop:
1. Prefer **Cloudflare Access**, **Cloudflare WARP**, or an org **VPN** so only
authenticated staff reach the service.
2. Bind to a specific interface only when necessary — never `0.0.0.0` / `::`
without understanding the exposure.
3. Set explicit override env vars only after access controls are in place:
- `WEBUI_ALLOW_PUBLIC_BIND=1` — acknowledges all-interface bind (`0.0.0.0`, `::`)
- `WEBUI_ALLOW_REMOTE_BIND=1` — acknowledges a non-loopback host
Startup **refuses** all-interface binds unless `WEBUI_ALLOW_PUBLIC_BIND=1`.
Non-loopback binds log a warning unless `WEBUI_ALLOW_REMOTE_BIND=1`.
## Environment variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `WEBUI_HOST` | `127.0.0.1` | Bind address |
| `WEBUI_PORT` | `8765` | Listen port |
| `WEBUI_REPO_ROOT` | repository root | Workflow/schema hash root for prompt library |
| `WEBUI_PROJECT_REGISTRY` | packaged JSON | Project registry file path |
| `GITEA_MCP_CONFIG` | unset | Server-side MCP profile config path (optional) |
| `GITEA_MCP_PROFILE` | unset | Active MCP profile name (optional) |
| `WEBUI_ALLOW_PUBLIC_BIND` | unset | Acknowledge `0.0.0.0` / `::` bind |
| `WEBUI_ALLOW_REMOTE_BIND` | unset | Acknowledge non-loopback bind |
Gitea credentials (`GITEA_TOKEN_*`, `.env.*`, keychain refs) are read only on
the server when a page needs live Gitea data (e.g. `/queue`). They are not
shipped to the browser.
## Health / deployment metadata
`GET /health` includes a `deployment` object with bind disposition, runtime
assumption paths, and the client-secret policy. Use it to verify an instance is
configured for internal-only operation.
## Non-goals (MVP)
- Full SSO or session login in the UI
- Hosting on the public internet without Access/VPN/WARP
- Embedding Gitea tokens in the frontend bundle
+15
View File
@@ -29,6 +29,13 @@ Optional environment variables:
|----------|---------|---------|
| `WEBUI_HOST` | `127.0.0.1` | Bind address (keep local for MVP) |
| `WEBUI_PORT` | `8765` | Listen port |
| `WEBUI_REPO_ROOT` | repository root | Prompt library workflow hash root |
| `WEBUI_PROJECT_REGISTRY` | packaged JSON | Project registry path |
| `GITEA_MCP_CONFIG` | unset | Server-side MCP profile config (never sent to browser) |
| `GITEA_MCP_PROFILE` | unset | Active MCP profile name (server-side only) |
See [webui-deployment.md](webui-deployment.md) for internal-only serving,
Cloudflare Access/WARP/VPN guidance, and unsafe bind overrides (#435).
## Routes (MVP)
@@ -134,10 +141,18 @@ health, workflow/schema SHA-256 hashes, and stale-runtime warnings when the
checkout is behind merged safety-gate changes. Restart guidance links to #420;
no tokens or MCP restart actions are exposed.
## Deployment boundary (#435)
MVP serves on loopback by default. Binding `0.0.0.0` or `::` is **refused**
unless `WEBUI_ALLOW_PUBLIC_BIND=1`. Non-loopback hosts log a warning unless
`WEBUI_ALLOW_REMOTE_BIND=1`. `GET /health` exposes `deployment` metadata.
## Tests
```bash
pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py tests/test_webui_gated_actions.py -q
pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py tests/test_webui_audit.py tests/test_webui_worktree_hygiene.py -q
pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py tests/test_webui_lease_visibility.py tests/test_webui_runtime_health.py -q
pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py tests/test_webui_deployment_boundary.py -q
```