BashaarJavaid/PortunusMCP

GitHub: BashaarJavaid/PortunusMCP

PortunusMCP 是一个零信任安全网关代理,通过身份作用域工具裁剪、schema 偏移检测、风险评分和防篡改审计追踪来填补 MCP 协议在授权与可审计性方面的安全缺口。

Stars: 0 | Forks: 0

# PortunusMCP **一个为 Model Context Protocol (MCP) 强制执行策略的网关代理** —— 提供基于身份作用域的工具可见性、schema 偏移(“rug pull”)检测、单次调用风险评分以及防篡改的签名审计追踪,且无需对上游 MCP server 进行任何更改。 [![ci](https://static.pigsec.cn/wp-content/uploads/repos/cas/99/993938d8ce5e902ccfb9d6747725c320d855dea3235ed9a304cedf0d94c9321f.svg)](https://github.com/BashaarJavaID/PortunusMCP/actions/workflows/ci.yml) ![coverage](https://img.shields.io/badge/coverage-%E2%89%A580%25%20(CI--gated)-brightgreen) ![python](https://img.shields.io/badge/python-3.12-blue) [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE) ![演示:工具裁剪、偏移检测与拦截、重放防护、策略模拟](https://static.pigsec.cn/wp-content/uploads/repos/cas/c4/c4074547e6f95d017203a4f9862e87916f06b550b2d3d86e7ce327fddb49635a.gif) *低权限身份只能看到允许其使用的工具。上游 server 在会话中途“rug-pull”(恶意篡改)了自己的 schema。网关将此偏移判定为 Critical,拦截了该调用,并将其挂起等待人工重新审批。对字节完全一致的重放请求予以拒绝。最后,针对刚刚发生的流量模拟了一份策略草案。* ## 为什么需要 MCP 定义了 LLM client 与工具 server 之间的 JSON-RPC 2.0 传输协议,但有意将授权、可审计性和完整性排除在范围之外——它假定部署的组织会自行构建该层。但在实践中几乎没人这么做,从而留下了三个具体的缺口: 1. **缺乏基于身份作用域的工具可见性。** 任何能够连接到 server 的 client 都会获得该 server 完整的 `tools/list`。原生并不存在“该用户只应看到一部分工具”的机制。 2. **Rug pulls(恶意篡改)。** Server 可以在人工于先前会话中批准某个工具的 schema *之后*对其进行更改,却没有任何机制来检测这种偏移。 3. **缺乏审计追踪。** 没有任何东西记录哪个身份、在哪个策略下、使用哪些参数调用了哪个工具,并能以某种形式让你日后证明它未被篡改过。 PortunusMCP 介于两者之间,填补了这三个缺口。 ### Client 兼容性 **双方均无需任何更改。** 上游 server 按原样进行代理,原生的 Claude Desktop、Cursor 或 SDK `ClientSession` 在默认的 `bearer` 认证模式下无需修改即可运行 —— RBAC、ABAC、偏移拦截、风险评分、参数验证以及签名审计追踪在每次调用中都会得到充分执行(集成测试套件端到端运行了一个未打补丁的 SDK client)。认证姿态是基于单个身份的([`ROADMAP.md`](./ROADMAP.md) 第 34 项):能够采用小型签名 client 的身份可选择开启 `signed` 模式,在该模式下,请求会携带一个非机密密钥 ID 以及基于该调用的 HMAC —— 传输过程中完全没有凭证,这使得重放防护得以真正实现(被截获的请求无法使用新的 nonce 重新签名)。这种权衡是真实的:`bearer` = 零 client 改动,密钥随请求传输;`signed` = 需自定义 client,防截获。 ## 架构 网关内的每个框都是 `services/gateway/` 中同名的模块;带有编号的阶段是 `ARCHITECTURE.md` §4.2 顺序中的决策流水线。时序图、部署图和数据流图位于 §4.4–§4.7。 ``` graph TD Client["MCP Client"] -->|"Streamable HTTP"| Interceptor subgraph Gateway["PortunusMCP Gateway process"] Interceptor["JSON-RPC Interceptor + Session Manager"] Interceptor --> Replay["1 Replay Guard"] Replay --> Auth["2 Auth / Identity"] Auth --> Policy["3+4 Policy Engine: RBAC + ABAC conditions + versioning"] Policy --> Drift["5 Drift Detector"] Drift --> Risk["6 Risk Engine"] Risk --> Validator["7 Param Validator"] Validator --> UpClient["Upstream Client"] Interceptor --> SchemaCache["Schema Cache + Pruner (tools/list)"] Interceptor --> AuditW["Audit Log Writer (hash-chained, ECDSA-signed)"] Approvals["Approvals lifecycle (admin API)"] Explainer["Decision Explainer (admin API)"] Simulator["Policy Simulator (admin API)"] end UpClient --> Srv["Upstream MCP Server (stdio subprocess, one per session)"] Replay --> Redis[("Redis: nonces, schema cache, risk counters, session TTL")] Risk --> Redis SchemaCache --> Redis Policy --> PG[("Postgres: audit_log, policy_versions, tool_baselines, approvals")] Drift --> PG AuditW --> PG Approvals --> PG Explainer --> PG Simulator --> PG Policy --> Rev["policies/revisions/ snapshots (rw submount)"] Simulator --> Rev Verifier["audit_verifier sidecar (separate process, read-only chain walk)"] --> PG ``` **多个上游在策略的 `servers:` 块中注册**(第 35 项):`server_id → stdio command`,并与其余策略一起进行版本控制和回滚。Client 连接到 `/mcp/`;一个会话绑定到一个上游,在连接时选定。RBAC 授权、偏移基线、schema 缓存、风险计数器和审批全部以真实的 `server_id` 为键,因此两个 server 上同名的工具被视为两个不同的工具。 ## 威胁模型(摘要) 包含整个模型所依赖假设的完整版本位于 [`THREAT_MODEL.md`](./THREAT_MODEL.md)。它明确指出了*未*涵盖的内容——一个诚实的范围界限远比隐式声称涵盖一切更有价值。 | 威胁 | 是否受保护? | 如何保护 / 为何不保护 | |---|---|---| | 已知身份对工具的未授权访问 | 是 | RBAC + ABAC 策略解析 | | 恶意 / 进行“rug-pulling”的 MCP server(schema 突变) | 是 | Drift Detector 对突变进行分类;High/Critical 级别在重新审批前于 `tools/call` 处被拦截 | | *已授权*身份在上下文中存在风险的调用 | 是 | Risk Engine —— 根据评分区间执行 挑战 / 人工审批 / 拒绝 | | 篡改审计日志 | 是 | Hash chain + 每行 ECDSA 签名;由仅持有公钥的 sidecar 独立验证 | | 截获请求的重放 | **`signed` 模式为是 / `bearer` 模式为部分** | `signed` 请求不携带凭证:字节完全相同的重放会被去重(`DENY_REPLAY`),且全新的 nonce 无法被重新签名(边缘层返回 401)。`bearer` 仅保留机会性去重 —— API key 包含在被截获的请求中 | | Tool Poisoning(描述中的对抗性文本) | **部分** | 审批后更改的描述在重新审批前会被拦截(默认为 High,第 36a 项);首次接触的基线会进行启发式扫描 —— 命中将被审计(`BASELINE_FLAGGED`)并提高后续所有调用的风险,但标记从不会拦截调用,且新颖的措辞能绕过模式列表。描述依然会原封不动地传达给 LLM —— 部分防护即为其上限 | | 通过工具*结果*进行的 prompt injection | 部分 | 协议层网关可以记录和限速,但无法对结果内容进行语义评估 —— 这是 client/agent-framework 的责任 | | 被盗的 API key | 部分 | 行为风险因素可减小爆炸半径;仅凭密钥本身无法与其持有者区分开来。`signed` 身份的密钥绝不会出现在传输过程中 —— 窃取它意味着主机环境已被攻破 | | 网关主机被攻破 | 否 | 攻击者拥有签名密钥 —— 这是基础设施加固问题,而非应用问题 | | 内部管理员滥用合法访问权限 | 否 | 只能做到事后可追溯且防篡改,而非事前预防;双人激活已设计,但尚未构建 | ## 运行演示 ``` python scripts/generate_signing_key.py # once: audit signing keypair (gateway won't start without it) python scripts/run_demo.py # resets demo state, mints keys, writes policies/demo-policy.yaml, waits ``` ``` # 在另一个终端中(恶意 upstream 命令存在于 demo policy 的 servers: block 中): POLICY_FILE=policies/demo-policy.yaml \ docker compose up -d --build # 当 driver 提示时 —— rug pull,故意显示在屏幕上: curl -X POST localhost:9800/_admin/apply_mutation # 当它再次提示时 —— 热加载收紧的 v2 policy 以完成 simulation finale: docker kill -s HUP portunusmcp-gateway-1 ``` 驱动程序以 `developer` 身份连接 —— 这是一个原生的 MCP client,任何地方都没有自定义的 `_meta`(只能看到 `send_email` / `read_inbox`;破坏性的 `delete_mailbox` 是*缺失的*,而不是被标记为禁用),随后以 `ops-admin` 身份连接(能看到全部三个工具)。它发起一次成功的调用,等待操作员的 mutation curl,然后展示被判定为 Critical 并被拦截(`DENY_DRIFT`)的偏移,管理员重新审批,相同的调用针对新 schema 成功执行,接着是 `signed` ci-agent 被截获的请求以字节完全一致的方式被重放(`DENY_REPLAY`),以及使用伪造的新 nonce 进行重放(HTTP 401 —— 截获的信息中不包含可用于重新签名的凭证),最后是一个策略模拟,针对 v2 草案重放演示本身的流量(`would_now_deny: 3`),然后打印 hash-chained 审计回执。 全部七个环节都是实时的 —— 没有任何脚本或造假。只有当操作员实际调用该 endpoint 时,才会触发突变,因此对抗性事件在镜头前清晰可见,而不是在屏幕外通过计时器发生。 之后,普通的 `docker compose up` 会故意拒绝启动:演示的策略 v1 在记录中具有不同的内容,fail-closed(故障即关闭)激活检查捕获了这一点。启动错误指明了解决方法 —— `docker compose run --rm gateway python scripts/reset_dev_state.py --yes`(仅限开发环境:清除本地审计链和演示状态,从不清除检查本身)。 **开发环境设置:** `python3.12 -m venv .venv && .venv/bin/pip install -e ".[dev]"`,然后运行 `.venv/bin/pytest`。完整命令列表见 [`CLAUDE.md`](./CLAUDE.md)。 ## 性能 以下数据是实测值而非估算值,测试时间为 **2026-07-10**,提交记录为 **`902341f`**,且完整的 §4.2 流水线均处于激活状态(重放 → 认证 → RBAC + ABAC → 偏移检测 → 包含全部八个因素的风险评分 → 参数验证 → 签名审计写入)。测试方法、硬件和复现步骤详见:[`ARCHITECTURE.md` §9](./ARCHITECTURE.md#9-performance-benchmarks)。 | 场景 | 直接调用 | 通过网关 | 开销 | |---|---|---|---| | 单次调用,已缓存 schema | 1.38 / 1.33 / 1.56 / 1.94 ms | 13.47 / 12.95 / 16.12 / 24.46 ms | 12.09 / 11.62 / 14.56 / 22.51 ms | | 单次调用,冷 schema 缓存 | 1.38 / 1.33 / 1.56 / 1.94 ms | 16.62 / 16.17 / 19.51 / 24.71 ms | — | | 10 个并发会话 (p95) | — | 160.20 ms | — | | 50 个并发会话 (p95) | — | 565.46 ms | — | | 100 个并发会话 (p95) | — | 1228.23 ms | — | | `tools/list` 负载(已裁剪身份) | 1506 B(未裁剪) | 797 B | **减少了 47.1%** | 延迟数值为 平均值 / p50 / p95 / p99。高并发下的 p95 主要受制于同步的 fail-closed 审计写入在 Postgres 连接池上的竞争,以及每个会话对应一个 stdio 子进程 —— 这两者都是已知的性能上限,已在 `ARCHITECTURE.md` §10 中讨论。 ## 技术栈 | 层级 | 选择 | 原因 | |---|---|---| | Runtime | Python 3.12, FastAPI + Starlette, 全面采用异步 | 第一方 MCP SDK;对于一个几乎完全处于 I/O 等待状态的代理来说是原生异步的 | | MCP 处理 | `mcp` 官方 Python SDK | 无需手动编写 JSON-RPC 帧格式 —— 而是直接在会话层进行拦截 | | 存储 | PostgreSQL 16(审计链、基线、审批、策略版本)+ Redis 7(nonce、schema 缓存、风险计数器) | 关系完整性对于 hash chain 至关重要;带有 TTL 的需求全部交由 Redis 处理 | | 策略 | YAML + Pydantic,辅以手动编写的 ABAC 表达式评估器(`ast.parse` + 节点白名单,不使用 `eval`) | 可通过 Git diff 对比,并在加载时进行验证。故意设计为非图灵完备 —— 无循环、无递归、无代码执行(关于为何不使用 OPA 参见 [ADR-004](./docs/adr/ADR-004-no-opa-for-v1.md)) | | 风险 | 固定的加权因子列表 + 行为 Redis 计数器 —— **按设计不使用 ML** | 操作员无法解释的安全决策,就是他们无法信任的安全决策 | | 加密 | SHA-256 hash chain + ECDSA P-256 每行签名 | 仅靠链条本身,任何拥有数据库写权限的人都能重新生成;但签名不行 | | 运维 | Docker Compose, Prometheus + Grafana(可选启用配置), structlog JSON 日志, GitHub Actions(在 80% 覆盖率门限下执行 ruff / mypy strict / pytest) | | ## 文档 - [`ARCHITECTURE.md`](./ARCHITECTURE.md) —— 决策流水线、每个组件的深入剖析、故障模式、可观测性、基准测试、可扩展性、测试、部署 - [`THREAT_MODEL.md`](./THREAT_MODEL.md) —— 受保护的内容、不受保护的内容以及背后的假设 - [`SECURITY.md`](./SECURITY.md) —— 漏洞披露 - [`docs/adr/`](./docs/adr/) —— 每个重大决策对应一个文件,包括为何在 v1 版本中各自拒绝使用 Envoy、OPA、Kong、NGINX、sidecars 和 client-SDK middleware - [`ROADMAP.md`](./ROADMAP.md) —— 以动态核对清单呈现的构建顺序 ## 路线图 阶段 1–3(核心网关 → 强化 → 风险与策略功能)已完成;阶段 4 是生产基础设施和最终定型工作。 **阶段 5 是将这个可运行的演示转化为 AppSec 团队能够真正采用的产品所需的工作**,它源于对已完成 v1 版本的对抗性自我审查:三个缺陷修复(过滤器绕过、重复的风险阈值、无上限的风险衰减),然后是针对单个身份的认证姿态,既恢复了原生 client 的兼容性,又通过将密钥移出传输通道真正实现了重放防护;此外还包括真正的多服务器注册表、工具描述完整性以及真正的 step-up auth(升级认证)。 [`ROADMAP.md`](./ROADMAP.md) 中的每一项都说明了证明其完成的检查标准,以及它所升级的威胁模型行 —— **当该行能够被如实地重写时,一项才算真正完成,而不是在代码合并时。** ## 许可证 MIT —— 详见 [`LICENSE`](./LICENSE)。
标签:API安全, DLL 劫持, JSONLines, JSON输出, MCP网关, Python, Streamlit, 大语言模型, 审计日志, 无后门, 版权保护, 访问控制, 零信任