MCP administration runbook
/admin/mcp is the administrator-only control plane for MCP service-account mappings, client tokens, policy, connection diagnostics, and audit evidence. This runbook describes the operational boundary delivered by PR #150 and PR #152. The Issue #151 evidence index records the exact runtime and visual checks for this documentation.
Security boundary
- The browser calls
/api/admin/mcp/*. The frontend BFF verifies Forgejo administrator membership before every request and forwards only bounded requests to the internalmcp-adminservice. - Admin BFF API requests at
/api/admin/mcp/*fail with401when anonymous and403when the authenticated Forgejo subject is not an administrator. The/admin/mcppage itself redirects anonymous users to login and returns404for non-admin users or insecure transport.mcp-adminhas no host-published port and is reachable only on the Compose network. - The re-authentication form verifies the current Forgejo password directly with Forgejo. The password is not stored, forwarded to the admin service, written to logs, or retained after the form is cleared. The resulting five-minute, HttpOnly proof is bound to the browser session and administrator subject.
- The internal BFF-to-admin credential is read as a Docker secret only by
mcp-admin. That service copies it with mode0440to the privateopenface-mcp-admin-bridgevolume; the frontend reads/run/mcp-admin-bridge/tokenread-only. The raw Docker secret is not mounted into the frontend and is never an environment variable or browser value. - Service-account credentials may reference only names in
OPENFACE_MCP_FORGEJO_TOKEN_ALLOWLIST. Each reference must be a readable, regular, non-symlink file directly belowOPENFACE_MCP_FORGEJO_TOKEN_ROOT. Do not add the internal admin credential to this allowlist. - Client token plaintext is returned only in a successful issue/rotate response to the already-authenticated BFF and is displayed once. Subsequent state, list, revoke, audit, and connection-test responses do not return it. After the dialog is closed or discarded, the UI no longer holds it and the registry, logs, and audit records do not retain recoverable plaintext.
Start the MCP profile
Create secrets and MCP state outside the repository. Compose's ./secrets/... defaults are convenient for local development but are not an external secret boundary. Set the three paths below to absolute locations before starting Compose. This Windows PowerShell example generates only the internal credential; use the deployment's secret-management procedure to populate the Forgejo token file.
$secretRoot = Join-Path $env:ProgramData 'OpenFace\secrets'
$stateRoot = Join-Path $env:ProgramData 'OpenFace\mcp-state'
New-Item -ItemType Directory -Force $secretRoot, $stateRoot | Out-Null
$hostIdentities = @([Security.Principal.WindowsIdentity]::GetCurrent().Name)
# If Docker Desktop uses another host identity, add that identity explicitly.
# $hostIdentities += 'CONTOSO\openface-docker'
$aclGrants = @($hostIdentities | ForEach-Object { '{0}:(OI)(CI)(F)' -f $_ })
foreach ($path in @($secretRoot, $stateRoot)) {
icacls.exe $path /reset /T /C | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Failed to reset existing ACLs under $path" }
icacls.exe $path /inheritance:r /T /C | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Failed to remove inherited ACLs from $path" }
icacls.exe $path /grant:r $aclGrants /T /C | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Failed to protect ACLs on $path" }
}
$internalTokenPath = Join-Path $secretRoot 'openface-mcp-admin-internal-token'
$forgejoTokenPath = Join-Path $secretRoot 'openface-mcp-forgejo-user-token'
$bytes = New-Object byte[] 48
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
try { $rng.GetBytes($bytes) } finally { $rng.Dispose() }
[Convert]::ToBase64String($bytes) | Set-Content -NoNewline -LiteralPath $internalTokenPath
$env:OPENFACE_MCP_ADMIN_INTERNAL_TOKEN_FILE = $internalTokenPath
$env:OPENFACE_MCP_FORGEJO_USER_TOKEN_FILE = $forgejoTokenPath
$env:OPENFACE_MCP_STATE_DIR = $stateRootStart or rebuild the bounded services, then inspect health without printing secret contents:
docker compose --profile mcp up -d --build frontend gateway openface-mcp mcp-admin
docker compose --profile mcp psDo not publish port 8001, bypass the BFF, or put secret values in docker-compose.yml, .env, screenshots, issue comments, or client configuration committed to Git.
Operator flow
- Open
/admin/mcpas a Forgejo administrator and complete re-authentication. If the proof is missing, expired, changed, or bound to another session or subject, the request must fail closed. - Add or select one service-account mapping. Choose the Forgejo user, an allowlisted secret reference, the smallest required scopes, and explicit repository permissions.
- Issue a client token with a subset of the mapping's scopes and repositories. Set the shortest practical TTL.
- While the one-time dialog is open, use Connection test to check
initialize,tools/list, andresources/list. The result separates reachability, HTTP/authentication failure, JSON-RPC failure, and usable tool/resource counts without echoing the token or upstream error text. - Copy the token only into a protected client secret store, then close or discard the dialog. Never put the plaintext in a ticket, shell history, screenshot, browser bookmark, or source file.
- When a mapping is disabled or remapped, verify that its previous mapping-version tokens are revoked. Rotating a token revokes its predecessor.
Safe client snippets
These examples intentionally contain placeholders only. Replace <OPENFACE_HOST> and <TOKEN_FILE> locally; never commit a real token.
Codex CLI
$env:OPENFACE_MCP_TOKEN_FILE = '<TOKEN_FILE>'
$env:OPENFACE_MCP_TOKEN = (Get-Content -LiteralPath $env:OPENFACE_MCP_TOKEN_FILE -Raw).Trim()
codex mcp add openface --url https://<OPENFACE_HOST>/mcp --bearer-token-env-var OPENFACE_MCP_TOKENClaude Desktop
{
"mcpServers": {
"openface": {
"command": "openface-mcp-stdio",
"env": {
"OPENFACE_MCP_REMOTE_URL": "https://<OPENFACE_HOST>/mcp",
"OPENFACE_MCP_CLIENT_TOKEN_FILE": "<TOKEN_FILE>"
}
}
}
}VS Code
{
"servers": {
"openface": {
"type": "http",
"url": "https://<OPENFACE_HOST>/mcp",
"headers": { "Authorization": "Bearer ${input:openface-token}" }
}
},
"inputs": [
{
"id": "openface-token",
"type": "promptString",
"description": "OpenFace MCP token",
"password": true
}
]
}Policy and audit
Policy updates include the displayed revision. A concurrent update returns a conflict; reload, inspect the new revision, and submit again. The default remains deny unless an explicit allow applies, and read-only rules deny matching writes.
Audit filters cover actual outcomes (allowed, denied, failed, replayed, and changed), subject, client, tool, time range, and bounded cursor pagination. Summary counts cover all matching records, not only the current page. Expanded records expose approved detail fields only; they do not return token plaintext, token digests, Forgejo PAT paths, idempotency fingerprints, or audit-chain hashes.
Recovery runbook
- Lost token: revoke it immediately, issue a replacement with the smallest scope, and update the client's protected token store.
- Suspected service-account compromise: disable the mapping first, rotate the Forgejo credential secret, remap the account, then issue new client tokens.
- Policy conflict: reload before retrying. Never overwrite a revision that was not reviewed.
- Admin backend unavailable: check
mcp-adminhealth, Docker secret mounts, and lifecycle/policy/audit volume permissions. Do not expose a public port to bypass the BFF. - Backup and restore: back up the token registry and lifecycle audit from the configured
OPENFACE_MCP_STATE_DIR, plus a consistent snapshot of the completeopenface-mcp-statevolume. The snapshot must include the policy and audit databases,/data/write-safety.sqlite3, and its adjacent.hmac-key; these preserve idempotency and operation-reconciliation history. Restoring only part of this state can repeat an upstream write whose earlier result was unknown. After restore, verify admin/non-admin access, issue a short-lived constrained token, run a connection test, revoke it, and inspect the audit outcome.
Release QA boundary
Manually inspect the real runtime at desktop and mobile widths in the base, solarpunk, and cyberpunk themes. Check tab wrapping, one-time-secret handling, contrast, controls, footer placement, and horizontal overflow. Screenshot generation is intentionally not a CI gate. The sanitized evidence for the current merged main is in Issue #151.
