docs(remote-mcp): threat model, trust boundaries, and decomposition ruling (Closes #956)
#930 inventoried stdio coupling; nothing stated what the adversary is, what each boundary protects, or why one process may hold credentials for several services. This adds that document as child 2 of epic #929. Adds docs/remote-mcp/threat-model.md covering 10 assets, the 5 adversaries #956 names, 9 trust boundaries, the data flows between them, a 14-entry per-boundary credential inventory, and an explicit decomposition ruling. Findings established from live native evidence at this commit: - Four prgs roles (reviewer, merger, reconciler, controller) resolve to one Gitea account, so separation of duty between approving and landing is enforced only by which process a call reaches. The mdcps tenant has no role separation at all: author, reviewer, and merger share one account. - Any one role process can resolve every other role's credential. gitea_list_profiles reports "credentials present" for other roles because it calls resolve_token on each one. - The Gitea server reads Jenkins and GlitchTip secrets out of the keychain to produce the "authenticated" word in gitea_audit_config's service summaries. - Jenkins and GlitchTip were already decomposed into separate MCP servers; the credential references were left behind in the Gitea configuration. Ruling D1 forbids a single integration process from holding credentials for unrelated services, with one time-boxed dual-run exception for the local fleet that expires with #939. D2 requires separation of duty to be credential-backed, D3 scopes credential resolution to the request principal, and D4 gives coordination state its own authority. Every #929 child from 2 through 10 is mapped to the boundary it implements. Anchors are enforced rather than asserted. #930's inventory anchors into gitea_mcp_server.py had already drifted between7bf4f125andaad5c8b4with nothing detecting it, so this change ships the guard that was missing: docs/remote-mcp/threat-model-anchors.json declares all 58 anchors with the substring each must contain, and tests/test_issue_956_threat_model.py fails if any anchor does not resolve, if the document cites an anchor the fixture does not cover, or if the structural obligations regress. Documentation only. No server behavior changes. Tests: - tests/test_issue_956_threat_model.py: 17 passed. - Four sabotage probes confirm the validator is not passing vacuously (shifted anchor, undeclared citation, broken count tally, and a blanked boundary owner). The last two probes exposed real weaknesses in the checks themselves, which were fixed: the section slice now stops at the next heading, and boundary ownership is read only from mapping table rows. - Docs-sensitive sweep (17 modules referencing docs/): 559 passed. - Full suite: 30 failed, 5799 passed, 6 skipped. All 30 reproduce on a clean base worktree at aad5c8b4; branch failures are a strict subset of base failures. No production file is modified by this change. Closes #956 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"_comment": [
|
||||
"Machine-checkable anchor table for docs/remote-mcp/threat-model.md (#956).",
|
||||
"Every file:line anchor cited in the threat model must appear here, and the",
|
||||
"source line at that anchor must contain the 'expect' substring.",
|
||||
"tests/test_issue_956_threat_model.py enforces both directions, so a refactor",
|
||||
"that shifts a line number fails the suite instead of silently rotting the",
|
||||
"document. #930's inventory had no such guard and its gitea_mcp_server.py",
|
||||
"anchors drifted between 7bf4f125 and aad5c8b4."
|
||||
],
|
||||
"generated_against_commit": "aad5c8b42361d380a8eeb07b94b90815e594c2c5",
|
||||
"anchors": [
|
||||
{"anchor": "gitea_mcp_server.py:24721", "expect": "bind_native_mcp_transport(transport=\"stdio\")"},
|
||||
{"anchor": "mcp_daemon_guard.py:45", "expect": "_PRODUCTION_TRANSPORTS = frozenset({\"stdio\"})"},
|
||||
{"anchor": "mcp_daemon_guard.py:174", "expect": "def bind_native_mcp_transport"},
|
||||
{"anchor": "irrecoverable_provenance.py:497", "expect": "def assess_transport_for_auth_mint"},
|
||||
{"anchor": "gitea_mcp_server.py:9129", "expect": "assess_transport_for_auth_mint()"},
|
||||
{"anchor": "gitea_mcp_server.py:9378", "expect": "assess_transport_for_auth_mint()"},
|
||||
{"anchor": "mcp_server.py:4", "expect": "Runs over stdio."},
|
||||
|
||||
{"anchor": "gitea_mcp_server.py:15412", "expect": "def _is_client_managed_process"},
|
||||
{"anchor": "gitea_mcp_server.py:15442", "expect": "def _provenance_mutation_block"},
|
||||
{"anchor": "gitea_mcp_server.py:15450", "expect": "unsupported_manual_launch"},
|
||||
{"anchor": "gitea_mcp_server.py:19001", "expect": "server_provenance"},
|
||||
{"anchor": "gitea_mcp_server.py:21442", "expect": "def _check_mcp_runtimes_diagnostics"},
|
||||
{"anchor": "gitea_mcp_server.py:21462", "expect": "\"ps\", \"-o\", \"pid,lstart,command\""},
|
||||
{"anchor": "gitea_mcp_server.py:21506", "expect": "\"ps\", \"eww\""},
|
||||
{"anchor": "gitea_config.py:1172", "expect": "RECOGNIZED_GITEA_ENV_KEYS"},
|
||||
{"anchor": "gitea_config.py:1233", "expect": "GITEA_CLIENT_MANAGED"},
|
||||
|
||||
{"anchor": "gitea_config.py:54", "expect": "ENV_PROFILE = \"GITEA_MCP_PROFILE\""},
|
||||
{"anchor": "gitea_config.py:97", "expect": "_REVIEW_MERGE_OPS"},
|
||||
{"anchor": "gitea_config.py:499", "expect": "repository authorization scope"},
|
||||
|
||||
{"anchor": "gitea_config.py:956", "expect": "def _keychain_token"},
|
||||
{"anchor": "gitea_config.py:974", "expect": "def resolve_token"},
|
||||
{"anchor": "gitea_config.py:1015", "expect": "def keychain_auth"},
|
||||
{"anchor": "gitea_config.py:294", "expect": "def _validate_identity_auth"},
|
||||
{"anchor": "mcp_daemon_guard.py:440", "expect": "def assert_keychain_access_allowed"},
|
||||
{"anchor": "gitea_mcp_server.py:19258", "expect": "def gitea_list_profiles"},
|
||||
{"anchor": "gitea_mcp_server.py:19309", "expect": "gitea_config.resolve_token(p)"},
|
||||
{"anchor": "gitea_mcp_server.py:19552", "expect": "def gitea_audit_config"},
|
||||
{"anchor": "gitea_mcp_server.py:19574", "expect": "service_summaries(config)"},
|
||||
|
||||
{"anchor": "gitea_config.py:704", "expect": "def resolve_service"},
|
||||
{"anchor": "gitea_config.py:837", "expect": "def service_summaries"},
|
||||
{"anchor": "gitea_config.py:851", "expect": "_keychain_token(auth.get(\"id\"))"},
|
||||
{"anchor": "gitea_mcp_server.py:17707", "expect": "\"jenkins-mcp\""},
|
||||
{"anchor": "gitea_mcp_server.py:17713", "expect": "external-mcp"},
|
||||
{"anchor": "gitea_mcp_server.py:17734", "expect": "\"glitchtip-mcp\""},
|
||||
{"anchor": "gitea_mcp_server.py:17739", "expect": "external-mcp"},
|
||||
{"anchor": "mcp_discoverability.py:9", "expect": "EXPECTED_JENKINS_TOOLS"},
|
||||
{"anchor": "mcp_discoverability.py:17", "expect": "EXPECTED_GLITCHTIP_TOOLS"},
|
||||
|
||||
{"anchor": "sentry_incident_bridge.py:36", "expect": "SENTRY_AUTH_TOKEN"},
|
||||
{"anchor": "sentry_incident_bridge.py:190", "expect": "def resolve_token"},
|
||||
{"anchor": "sentry_incident_bridge.py:289", "expect": "Authorization"},
|
||||
{"anchor": "sentry_observability.py:55", "expect": "SENTRY_DSN"},
|
||||
|
||||
{"anchor": "master_parity_gate.py:168", "expect": "def capture_startup_parity"},
|
||||
{"anchor": "master_parity_gate.py:255", "expect": "mutation_safe"},
|
||||
{"anchor": "gitea_mcp_server.py:19102", "expect": "def gitea_assess_master_parity"},
|
||||
|
||||
{"anchor": "gitea_mcp_server.py:190", "expect": "ACTIVE_WORKTREE_ENV"},
|
||||
{"anchor": "gitea_mcp_server.py:191", "expect": "AUTHOR_WORKTREE_ENV"},
|
||||
{"anchor": "gitea_mcp_server.py:2348", "expect": "/tmp/gitea_issue_lock.json"},
|
||||
{"anchor": "gitea_mcp_server.py:10894", "expect": "def gitea_bootstrap_author_issue_worktree"},
|
||||
{"anchor": "mcp_server.py:10", "expect": "/tmp/mcp_server_stderr.log"},
|
||||
|
||||
{"anchor": "issue_lock_store.py:26", "expect": "DEFAULT_LOCK_DIR"},
|
||||
{"anchor": "issue_lock_store.py:83", "expect": "def session_pointer_path"},
|
||||
{"anchor": "issue_lock_store.py:98", "expect": "def is_process_alive"},
|
||||
{"anchor": "mcp_session_state.py:27", "expect": "DEFAULT_STATE_DIR"},
|
||||
{"anchor": "control_plane_db.py:47", "expect": "DEFAULT_DB_PATH"},
|
||||
{"anchor": "control_plane_db.py:380", "expect": "mode=0o700"},
|
||||
{"anchor": "control_plane_db.py:386", "expect": "sqlite3.connect"},
|
||||
{"anchor": "control_plane_db.py:1145", "expect": "os.getpid()"},
|
||||
{"anchor": "gitea_mcp_server.py:12801", "expect": "owner_pid_alive"}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user