Files
Gitea-Tools/docs/webui-deployment.md
T
jcwalker3andClaude Opus 4.8 479e434f92 feat(webui): console authorization, RBAC, redaction, and audit model (Closes #633)
Phase 1 of the MCP Control Plane Web Console (#631) defines the model that
future gated writes must pass through, and enables none of them.

The read-only MVP (#426-#436) ships with no authentication; protection comes
from network placement alone (#435). That is adequate while every route is a
GET and inadequate the moment Phase 2 wires a write. This lands the authority
first, so no write can later be added without something to check it against.

webui/console_authz.py
  Identity sources (none / local-dev / access-proxy), a four-role matrix
  (viewer, operator, controller, admin), the privileged-action list, and a
  fail-closed authorize(). Every action maps to a task_key in
  task_capability_map, so the console cannot invent an authority the MCP layer
  does not already define. Roles are always server-side configuration, never a
  client assertion. Deny reasons are closed and enumerated; there is no
  implicit allow branch, and even an allow reports execution_enabled=false
  while ACTIVE_PHASE is 1.

webui/console_redaction.py
  One redaction pass for API payloads, rendered HTML, logs, and audit records.
  Reuses gitea_audit.redact as the shared authority rather than forking it,
  then adds console patterns for keychain references, credential assignments,
  PEM private-key blocks, and JWTs. Never raises: an unredactable value
  degrades to the placeholder rather than being emitted raw.

webui/console_audit.py
  Console-side audit records, which gitea_audit cannot supply: it records MCP
  mutations and carries no console actor, identity source, correlation id, or
  retention class, and an authorization denial is not a mutation at all. The
  two are additive and join on correlation.request_id. Records are redacted at
  build time, re-scanned at write time, and dropped rather than persisted if
  they still trip a detector. Retention is per-record; an unknown action is
  retained as privileged rather than standard.

webui/app.py
  Attaches an authorization block to the existing preview and attempt routes
  and records the decision. The terminal outcome is unchanged - gated_actions
  still fails closed for every action - so this cannot loosen anything. Adds
  GET /api/console/security-model publishing the three policies as JSON.

Probe authentication is deliberately declarative in this slice:
probe_auth_required() reports operator intent and no route consults it. The
documentation says so plainly and a regression test pins the not-enforced
status, so wiring it in Phase 2 is a deliberate change rather than a silent
one. An operator who sets the variable believing it protects a probe would be
worse off than one who knows it does not.

Tests: tests/test_webui_console_authz_audit.py - 75 passed, 93 subtests,
covering each acceptance criterion and each test the issue requires
(redaction units, default-deny for unauthenticated write stubs, audit record
creation for a simulated privileged preview).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-22 14:37:35 -05:00

62 lines
2.7 KiB
Markdown

# 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.
The authorization, RBAC, redaction, and audit model that future gated writes
must pass through is defined in
[`webui-authz-audit.md`](webui-authz-audit.md) (#633).
- **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