揭示MCP协议缺乏认证和授权机制,自托管MCP服务器存在工具调用无边界的安全风险,并提供了作者自建的安全加固方案。
MCP(Model Context Protocol)是 AI Agent 连接工具的方式。Claude Desktop 使用它,Cursor 使用它,成千上万的开发者正在构建 MCP 服务器,让 AI 访问他们的 API、数据库和基础设施。
但有一个问题:MCP 没有安全模型。
协议定义了客户端如何与服务器通信,但完全没有规定该服务器允许做什么。没有客户端与服务器之间的认证。没有对哪些工具可以被调用的授权。没有关于发生了什么事的审计日志。规范假设你得自己处理这一切。
我运行着一台自托管服务器,上面有 Prometheus、Grafana、Ollama、Gitea 和其他一些服务。我想让 Claude Desktop 通过 MCP 查询所有这些服务。标准做法是为每个服务写一个 Python FastMCP 服务器——每个服务几十行代码,硬编码 API 密钥,注册工具,完成了。
这在你不考虑你实际构建了什么的时候是可行的:
每个 MCP 服务器都有权访问其进程可触及的一切。你的 Prometheus 工具同时可以访问你的 Grafana API、你的 Gitea API,以及 localhost 上的任何其他服务。没有作用域隔离。
API 密钥存在于环境变量或配置文件中。如果你有 9 个 MCP 服务器,你就有 9 处凭据以明文形式存放,没有访问策略。
没有任何日志记录。如果 Claude 调用了一个重启服务或删除数据的工具,没有任何记录表明哪个工具被调用了、带了什么参数、由哪个 Agent 调用、何时调用。
没有只读与读写的区分概念。一个工具要么存在要么不存在。MCP 不知道 query_prometheus 可以自由安全地调用,但 restart_service 应该需要审批。
工具组合会产生涌现风险。当 Claude 可以访问多个 MCP 服务器时,它可以在它们之间链式调用。服务器 A 读取敏感数据,服务器 B 发帖到外部 API——Claude 可以用一种两个服务器都没有预料到的方式将它们组合起来。
这些不是理论风险。在开发过程中,我将一个 Agent 声明为只读(Trust Tier 1),但给了它一个使用 HTTP POST 的工具。我构建的系统捕获了它——阻止了调用,记录了信任违规,迫使我要么修复配置,要么明确升级信任级别。没有这种强制执行,该工具本会静默工作,而我永远不会知道我的安全模型是错的。
Heddle 是一个运行时,位于你的 YAML 配置和 MCP 协议之间。你在配置文件中定义工具,Heddle 对它们进行验证、保护和服务——每次调用都执行策略强制。
以下是一个 Prometheus 的完整工具服务器:
agent:
name: prometheus-bridge
version: "1.0.0"
exposes:
- name: query_prometheus
access: read
description: "Run a PromQL query"
parameters:
query: { type: string, required: true }
- name: get_alerts
access: read
description: "List active Prometheus alerts"
http_bridge:
- tool_name: query_prometheus
method: GET
url: "http://localhost:9090/api/v1/query"
query_params: { query: query }
- tool_name: get_alerts
method: GET
url: "http://localhost:9090/api/v1/alerts"
runtime:
trust_tier: 1
运行 heddle run agents/prometheus-bridge.yaml,Claude 就可以用自然语言查询 Prometheus。但每次调用在到达 API 之前都要经过一个六层调度管道:
速率限制 → 访问模式检查 → 升级规则 → 信任级别强制 → 输入验证 → HTTP bridge 执行
每一层都可以独立地阻止调用并记录原因。
调度管道对每次工具调用强制执行以下控制:
信任级别(T1—T4)。 每个配置声明一个信任级别。T1(观察者)只能使用 GET——任何 POST/PUT/DELETE 在运行时被阻止,而不仅仅是警告。T2(工作者)允许有范围的写入。T3(操作员)允许跨 Agent 调用。T4(特权)需要人工审批。我用这个机制捕获了一个真实的错误配置——一个 T1 Agent 试图 POST,强制执行器在请求离开进程之前就阻止了它。
访问模式注解。 每个工具都声明为 access: read 或 access: write。带有写工具的 T1 配置在加载时被拒绝——服务器甚至还没启动。这是最低权限的 schema 层面版本。
凭据代理。 API 密钥存储在 ~/.heddle/secrets.json 中,每个配置有独立的访问策略。配置通过 {{secret:prometheus-token}} 引用它们——在运行时解析,绝不会写入 YAML 文件。配置只能访问它被明确授予的密钥。未授权的访问被拒绝、记录,且调用在任何 HTTP 请求被构建之前中止——故障关闭。(它最初不是这样设计的:发布版本的拒绝secret返回一个占位符字符串,让请求继续进行。一个外部安全审查发现了它。更多内容见下文。)
升级规则。 声明性条件,用于搁置工具调用以待审查,而不是执行它。例如,我的 VRAM 编排器有一个规则,当任何 smart_load 调用的 model_name 包含 "27b" 时就将其搁置——因为加载一个 270 亿参数的模型会消耗我 24GB GPU 内存的大部分。规则触发,调用被搁置,审计日志记录原因。
escalation_rules:
- name: large-model-load
reason: "Loading a model that will consume most of the 24GB VRAM"
tool: "smart_load"
param_contains:
model_name: "27b"
输入验证。 对每个参数进行类型检查、长度限制和注入模式检测。验证器捕获 shell 注入(; rm -rf /)、SQL 注入(' OR 1=1)、路径遍历(../../etc/passwd)和 LLM prompt 注入(ignore previous instructions)。在严格模式下,这些被阻止。在 permissive 模式下,它们被记录并通过。
哈希链审计日志。 每次工具调用、信任违规、凭据访问和升级搁置都被记录为 JSON Lines 条目。每个条目包含前一个条目的 SHA-256 哈希——如果有人修改或删除了日志条目,链就断了,验证失败。
配置签名。 所有 YAML 配置都用 HMAC-SHA256 签名。如果配置在签名后被修改,运行时检测到篡改。AI 生成的配置(来自 Heddle 的自然语言生成器)自动被隔离在 staging 目录中,直到明确提升。
我目前通过一个 MCP 连接(到 Claude Desktop)运行着来自 9 个活动配置的 46 个工具(总共 11 个配置;两个因不兼容的传输方式被排除)。配置覆盖了 Prometheus、Grafana、Ollama、Gitea、一个 RSS 聚合器、一个 RAG 搜索 API、一个多模型 AI 平台桥接器、一个 GPU VRAM 编排器,以及一个日常运维简报 Agent。
这 46 个工具中的每一个都经过相同的调度管道。Prometheus 工具是 T1(只读,五个工具)。Ollama 桥接器是 T2(可以为文本生成 POST)。VRAM 编排器是 T3(可以调用其他 Agent,对破坏性操作有升级规则)。
信任级别不仅仅是标签——它们是被强制执行的。一个 T1 配置在物理上无法发出 POST 请求,即使 HTTP bridge URL 是正确的且 API 会接受它。强制执行器在请求被构建之前就阻止了它。
发布一个安全工具意味着你有义务让人们尝试破解它。在 v0.2.0 之后,我将代码库交给两个独立的 AI 辅助安全审查——不同的前沿模型,没有共享上下文——加上我自己的手动审查,并要求他们找出最糟糕的问题。
他们交付了。四个发现,都是真实的:
最常用的路径绕过了管道。 自定义的 stdio 处理器——Claude Desktop 使用的精确传输方式——直接分派工具调用,完全跳过了六层管道。安全模型在 HTTP 服务器上是严密的,而在最重要的路径上完全缺失。现在每条执行路径都通过一个 ToolPolicy.guard() 路由,并且存在 13 个对抗性测试来证明这个绕过始终被关闭。
凭据拒绝故障开放。 如上所述:一个被拒绝的 secret 产生一个占位符,请求仍然发出去了。面对一个宽松的上游,一个未认证的调用可能静默成功。CredentialDenied 现在在一个请求对象被构建之前就中止调用。
脱敏有漏洞,而哈希链使其永久化。 审计脱敏器没有递归到嵌套结构中,而且它是通过令牌模式而不是按 key 解析查询参数来清除 URL 的。这在这里比在普通日志中更重要:哈希链条目不能在事后脱敏,否则会破坏 verify_chain()。泄露的 secret 将是 tamper-evident 的永久记录。脱敏现在递归处理,URL 按结构解析。
处理器 codegen 使用了 exec()。 HTTP-bridge 处理器被生成为源字符串并用 exec() 执行。现在没有任何受配置影响的字符串到达 exec()——处理器是通过 inspect.Signature 构建的真实类型化可调用对象。
所有这些都作为 v0.2.1 发布,一个保证性发布:没有新功能,每个更改都关闭了一个发现,测试数量从 237 增加到 273,几乎全部是回归测试,用于固定这些修复。
令人不安的部分:凭据故障开放在本文的第一稿中被描述为一个功能。这是我所知道的外部审查最强的论据——作者是唯一保证阅读设计意图而不是行为的人。
每个安全控制都映射到至少一个行业框架。(OWASP 在 2025 年 12 月重新编号了 Agentic 风险——此表使用来自 Agentic Applications Top 10 的当前 ASI 标识符。)如果你在一家需要展示合规性的组织中,或者如果你在构建一个展示应用安全架构的作品集(这是我构建它的原因),这很重要:
完整的威胁模型在仓库的 docs/threat-model.md 中。
git clone https://github.com/goweft/heddle.git
cd heddle
python -m venv venv && source venv/bin/activate
pip install -e ".[dev]"
# Try a starter pack
cp packs/prometheus.yaml agents/
heddle validate agents/prometheus.yaml
heddle run agents/prometheus.yaml --port 8200
Heddle 附带了 6 个 starter pack——Prometheus、Grafana、Gitea/GitHub、Ollama、Sonarr 和 Radarr——你可以直接放入 agents/ 并立即运行。除 Ollama(T2 用于文本生成)外都是只读(T1)。
或者用自然语言生成配置:
heddle generate "agent that wraps the Home Assistant API" --model qwen3:14b
适用于 Claude Desktop、Cursor 和任何支持 stdio 传输的 MCP 客户端。
下一个版本使 Heddle 自己的策略异常变成有生命周期的。ADR 005 提议与权限成反比例缩放的过期范围(T4 授权存活 30 天,而不是永久),绑定到配置签名的凭据授权以便重新签名的配置使其批准失效,以及 revoke_when 谓词,在其正当假设不再成立的那一刻在调度时撤销授权。不变式:每个异常都精确地携带一个收集者——一个日期、一个签名引脚、一个谓词或一次协调传递。没有人能收集的承诺不是控制。
Heddle 是开源的(MIT),位于 github.com/goweft/heddle。273 个测试、15 个安全控制,以及映射到 OWASP Top 10 for Agentic Applications (2026) 和 NIST AI RMF 的威胁模型。如果你正在向 AI Agent 暴露 API,我想知道你还希望存在什么安全控制。