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

2.7 KiB

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 (#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