MCP管理Runbook
/admin/mcp は、MCPのサービスアカウント対応付け、クライアントToken、policy、接続診断、監査証跡を扱う管理者専用control planeです。PR #150 と PR #152 で実装された運用境界を説明します。今回の実runtimeとVisual QAの詳細は Issue #151の証跡index に固定しています。
セキュリティ境界
- ブラウザは
/api/admin/mcp/*を呼びます。frontend BFFはリクエストごとにForgejo管理者権限を確認してから、制限されたリクエストだけを内部のmcp-adminへ転送します。 /api/admin/mcp/*のadmin BFF API requestは、未ログインなら401、認証済みでもForgejo管理者でなければ403です。/admin/mcppage自体は未認証ならloginへredirectし、非管理者または安全でないtransportなら404を返します。mcp-adminはhost portを公開せず、Compose networkからだけ到達できます。- 再認証formは現在のForgejo passwordをForgejoへ直接照合します。passwordは保存、admin serviceへの転送、log出力をせず、formを消去した後も保持しません。発行される5分間のHttpOnly proofはbrowser sessionと管理者subjectに束縛されます。
- BFFからadminへの内部credentialは
mcp-adminだけがDocker secretとして読みます。mcp-adminはmode0440でprivateなopenface-mcp-admin-bridgevolumeへcopyし、frontendは/run/mcp-admin-bridge/tokenをread-onlyで読みます。raw Docker secretをfrontendへmountせず、環境変数やbrowser valueにも置きません。 - サービスアカウントのcredential参照名は
OPENFACE_MCP_FORGEJO_TOKEN_ALLOWLISTにあるものだけを許可します。参照先はOPENFACE_MCP_FORGEJO_TOKEN_ROOT直下のreadableなregular fileかつnon-symlinkでなければなりません。内部admin credentialをallowlistへ追加しないでください。 - client Tokenの平文は、認証済みBFFへのissue/rotate成功responseだけで返され、一度だけ表示されます。その後のstate、list、revoke、audit、connection-test responseは平文を返しません。dialogを閉じるか破棄した後、UIは平文を保持せず、registry、log、auditにも復元可能な平文を残しません。
MCP profileを起動する
secretとMCP stateはrepositoryの外で作成します。Composeの./secrets/... defaultはlocal development用で、repository外のsecret境界にはなりません。Compose起動前に次の3つのpathをabsolute pathへ設定します。Windows PowerShellの例は内部credentialだけを生成するため、Forgejo token fileはdeploymentのsecret管理手順で投入してください。
$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)
# Docker Desktopが別のhost identityを使う場合だけ、そのidentityを明示的に追加します。
# $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 = $stateRootbounded serviceを起動またはrebuildし、secretの中身を表示せずにhealthを確認します。
docker compose --profile mcp up -d --build frontend gateway openface-mcp mcp-admin
docker compose --profile mcp ps8001を公開portにせず、BFFを迂回せず、docker-compose.yml、.env、screenshot、Issue comment、Gitへcommitするclient設定にsecret値を置かないでください。
操作手順
- Forgejo管理者で
/admin/mcpを開き、再認証を完了します。proofがない、期限切れ、改変、別session、別subjectに束縛されている場合はfail closedで拒否されなければなりません。 - サービスアカウント対応付けを追加または選択します。Forgejo user、allowlist済みsecret参照名、必要最小限のscope、明示したrepository権限を選びます。
- 対応付けのscopeとrepositoryのsubsetだけを持つclient Tokenを発行します。TTLは実運用で必要な最短値にします。
- 一度きりのdialogを開いたまま接続を確認を実行し、
initialize、tools/list、resources/listを確認します。到達性、HTTP/認証失敗、JSON-RPC失敗、利用可能なtool/resource数を区別し、Tokenや上流error本文をechoしません。 - Tokenは保護されたclient secret storeへコピーしたらdialogを閉じるか破棄します。平文をticket、shell history、screenshot、browser bookmark、source fileへ置かないでください。
- 対応付けを無効化または再マッピングしたときは、以前のmapping versionに紐付くTokenが失効したことを確認します。Tokenのrotateでは前のTokenが失効します。
安全なclient snippet
以下はplaceholderだけを含む例です。<OPENFACE_HOST>と<TOKEN_FILE>はローカルで置換し、実Tokenをcommitしないでください。
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と監査
Policy更新には画面に表示されたrevisionを含めます。同時更新があればconflictになるため、再読み込みして新しいrevisionを確認してから再送します。既定はdenyで、明示的なallowだけが適用され、read-only ruleは一致するwriteを拒否します。
監査は実際のoutcome(allowed、denied、failed、replayed、changed)、subject、client、tool、期間、bounded cursorで絞り込めます。summaryは現在ページだけでなくfilterに一致する全recordを数えます。展開recordは承認済みfieldだけを返し、Token平文、Token digest、Forgejo PAT path、idempotency fingerprint、audit-chain hashは返しません。
障害対応Runbook
- Token紛失: 直ちに失効し、最小scopeで再発行して、clientの保護されたsecret storeを更新します。
- サービスアカウント侵害の疑い: 先に対応付けを無効化し、Forgejo credential secretをrotateし、再対応付けしてから新しいclient Tokenを発行します。
- Policy conflict: 再読み込みしてから再実行します。未確認のrevisionを上書きしません。
- Admin backend停止:
mcp-adminのhealth、Docker secret mount、lifecycle/policy/audit volumeの権限を確認します。BFFを迂回する公開portは追加しません。 - Backupとrestore: 設定した
OPENFACE_MCP_STATE_DIRのToken registryとlifecycle auditに加え、openface-mcp-statevolume全体を一貫したSQLite snapshotとしてbackupします。snapshotにはpolicy/audit database、/data/write-safety.sqlite3、隣接する.hmac-keyを必ず含めます。これらはidempotencyとoperation-reconciliation historyを保持するため、一部だけrestoreすると、以前に上流で成功したか不明なwriteを再実行する可能性があります。restore後は管理者/非管理者のaccess、短命で制限されたTokenの発行、接続確認、失効、audit outcomeを確認します。
Release QAの境界
base、solarpunk、cyberpunk themeのdesktop/mobile幅を実runtimeで手動確認します。tabのwrap、一度きりのsecret処理、contrast、control、footer位置、horizontal overflowを確認します。screenshot生成は意図的にCI gateにしません。今回のmerged mainに対するマスク済み証跡は Issue #151 にあります。
