关于 security-proxy
security-proxy 是部署在 MCP 服务前的安全网关,可统一执行提示注入防护、PII 遮蔽和审计。它还支持多服务策略、工具漂移检测及跨服务毒性数据流拦截。
主要功能
- 01检测提示注入与工具结果注入
- 02遮蔽响应中的 PII 和密钥
- 03记录可配置审计日志
- 04以统一策略代理多个 MCP 服务
- 05拦截跨服务毒性数据流
- 06检测工具定义漂移
适用场景
- 01为现有 MCP 服务添加安全层
- 02统一管理多个 MCP 服务的访问策略
- 03阻止敏感数据流向外部工具
- 04审计 Agent 的工具调用
MCP 相关
作为 MCP 网关,它位于客户端与一个或多个 MCP 服务之间,转发工具和提示词,并按策略检查调用及结果。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
@guardbee/mcp-security-proxy
🇬🇧 English | 🇹🇷 Türkçe | 🇨🇳 中文 | 🇯🇵 日本語 | 🇰🇷 한국어 | 🇧🇷 Português | 🇪🇸 Español | 🇩🇪 Deutsch
一个透明的安全层,位于任意 MCP 服务之前。它可以阻止提示词注入攻击、屏蔽响应中的个人身份信息,并将每个请求写入不可篡改的审计日志。
默认情况下,遥测功能处于关闭状态。设置
GUARDBEE_TELEMETRY=1后,它会发送服务、工具、成功状态、耗时和简短参数值(例如文件路径);较长的字符串以及content、token或password等字段会替换为占位符。请参阅@guardbee/mcp-telemetry。
Claude ──► MCP Security Proxy ──► Any MCP Server
│
├─ Prompt injection detection (27 rules, EN + TR)
├─ PII + secret masking (TC, VKN, IBAN, card, phone, 22 key formats)
├─ Block or warn mode
├─ Toxic-flow (lethal trifecta) blocking
└─ Hash-chained audit log
功能
- 提示词注入防护 — 提供 27 条英语和土耳其语规则;精准规则会拦截,通用短语只会发出警告
- 工具结果注入 — 对工具结果应用相同的模式,标记为 MCP06:2025。拦截模式会替换结果;警告模式会在结果前添加警告
- 会话工具固定 — 固定首次
tools/list的结果。之后的描述或架构变更属于暗中替换(MCP03:2025)。拦截模式会继续提供固定定义,并拒绝与变更后定义不符的调用 - 个人身份信息屏蔽 — 屏蔽响应中的 TC Kimlik No、VKN、IBAN、银行卡号和电话号码(通过校验和验证)、护照号、SGK、MERSİS、出生日期、邮政地址和 KEP 地址、电子邮件、可选的 IP/MAC/车牌号、22 种提供商密钥格式、私钥和连接字符串
- 拦截 / 警告模式 — 每个拦截器都可以独立运行于拦截或警告模式
- 审计日志 — 可将日志写入控制台或文件
- 网关模式 — 通过一个代理接入多个 MCP 服务,并使用一份 YAML 策略(允许 / 拒绝 / 屏蔽 / 警告)
- 毒性流程拦截 — 跨服务跟踪致命三元组,并拦截会使其完整的出站调用
- 大模型 API 网关 — 兼容 OpenAI 和 Anthropic 的端点:使用网关密钥代替提供商密钥;对提示词进行个人身份信息屏蔽和注入检查;设置 Token 和支出额度;并在审计日志中记录用量与成本
- A2A 网关 — Agent 之间的 Agent2Agent 调用经由网关:调用方需进行身份验证;消息和工件中的注入内容会被拦截;个人数据会被屏蔽;会拒绝推送通知 webhook 向专用网络发送请求;并审计每次调用
- 编码 Agent 钩子 —
guardbee-hook将相同策略应用于 Claude Code 和 Cursor 自带的 shell、文件和 Web 工具 - 零代码更改 — 可接入任何现有 MCP 服务之前
快速开始
npm install -g @guardbee/mcp-security-proxy
添加到 claude_desktop_config.json:
{
"mcpServers": {
"secure-filesystem": {
"command": "npx",
"args": [
"-y", "@guardbee/mcp-security-proxy",
"--", "npx", "-y",
"@modelcontextprotocol/server-filesystem", "/tmp"
]
}
}
}
代理会将
--后面的内容作为目标 MCP 服务启动。
网关模式:多个服务,一套策略
使用 guardbee-proxy.yaml 将每个 MCP 服务接入同一个代理。init 命令会替你完成迁移:
npx -y @guardbee/mcp-security-proxy@^1 init --client claude-desktop # or cursor, claude-code, --file ./mcp.json
它会将每个 stdio 服务移入 ~/.guardbee/guardbee-proxy.yaml(模式为 0600),备份客户端配置,并将其指向代理。HTTP 服务保持原样并会被报告,因为它们会绕过策略。--dry-run 会打印这两个文件,但不写入。也可以手动操作:
{
"mcpServers": {
"guardbee": {
"command": "npx",
"args": ["-y", "@guardbee/mcp-security-proxy@^1", "--config", "/path/to/guardbee-proxy.yaml"]
}
}
}
version: 1
upstreams:
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env: { GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}" } # read from the environment
postgres:
command: npx
args: ["-y", "@guardbee/mcp-db-gateway"]
labels: # override the heuristic labels
github__get_issue: [untrusted]
github__create_pull_request: [egress]
tools: # what the agent sees (globs on <upstream>__<tool>)
hide: ["*__delete_*"]
descriptions:
github__create_issue: "Create an issue in acme/* repositories only."
rules: # first match wins
- id: no-deletes
match: { tool: "postgres__delete_*" }
action: deny # allow | deny | mask | warn | approve
- match: { tool: "postgres__query", args: { table: "salaries" } } # dotted path → value, strings are globs
action: mask
mask: { fields: [salary, iban] } # blank these JSON keys
- match: { label: destructive }
action: approve # ask the person first
taint:
mode: strict # strict | approve | warn | off
basis: capability # capability | data — data: block only egress calls that carry sensitive data
interceptors:
anomaly: # off unless set; limits per session, sliding window
enabled: true
action: warn # warn | block
callBurst: { count: 60, windowSeconds: 60 }
audit:
sink: file
filePath: ./guardbee-audit.jsonl
- 工具和提示词会显示为
<upstream>__<tool>。 - 有害数据流(致命三要素): 每个工具都会被标记为
untrusted、sensitive、egress或destructive(根据名称/描述启发式判断,可在labels下覆盖)。一旦会话读取了不可信内容(来自untrusted工具或任意资源)和敏感数据(来自sensitive工具,或结果中包含任意 PII),strict模式下会阻止egress调用,warn模式下则只记录日志。此检查跨服务进行:从 GitHub 读取的问题,以及从 CRM 获取的客户记录,会导致对第三个服务上的 webhook 调用被阻止。 - 规则可按
tool(glob)、upstream、label、session(clean|tainted)和args匹配。mask会强制对该工具执行 PII 屏蔽,即使屏蔽功能处于关闭状态;mask.fields还会将text和structuredContent中指定的 JSON 键置空。规则的allow不会跳过有害数据流检查;请改为重新标记该工具。 - 主题(
topics、match.topic): 规则可以指定主题,而不是工具——“禁止通过此 Agent 发送任何有关并购或薪资的内容”。主题由一组词组成;当出现不同词的数量达到minMatches(默认值为 1)时,文本即匹配主题。匹配时会忽略大小写并折叠土耳其语字母(BİRLEŞME=birlesme),词语可以带后缀(maaş可匹配maaşları,satın alma可匹配satın almayı);将词语放在引号内即可只匹配完整词('"AI"')。规则会检查调用参数,并再次检查返回内容——工具结果、资源或提示词——因此即使主题只出现在回答中也会被捕获:deny会扣留内容,mask会强制执行 PII 屏蔽和mask.fields,warn会记录日志,approve会请求批准(只请求一次:对于已获批准的调用所返回的回答不会再次请求)。在 LLM API 网关中,主题规则会应用于整个提示词,使用tool: llm:<model>;由于 API 调用无人可询问,approve在此会拒绝请求。topics: - id: ma terms: [birleşme, "satın alma", "due diligence", acquisition, merger] - id: salary terms: [maaş, bordro, payroll, salary] minMatches: 1 rules: - { id: no-ma-out, match: { topic: ma, label: egress }, action: deny } - { id: contractors, match: { topic: salary, user: "*@contractor.com" }, action: approve } - { id: ma-to-llm, match: { topic: ma, tool: "llm:gpt-*" }, action: deny } - 工具暴露(
tools):expose是针对 Agent 所见名称的 glob 允许列表(未设置时:所有工具),hide则会在此基础上移除工具。隐藏的工具不会出现在tools/list中,因此模型无法读取其描述(缩小投毒面并减少 token),对其发起的调用会被拒绝,并计入anomaly.repeatedBlocks。descriptions会用你编写的描述替换工具描述;漂移检测仍会监控服务自身的描述。与在调用时拒绝请求的deny规则不同,隐藏会让 Agent 无法看到该工具。 - **
tools/list中的工具投毒(interceptors.toolPoisoning,默认开启):**Agent 查看每个工具定义之前,都会对其进行扫描——包括描述(若你在tools.descriptions下编写了描述,则扫描你编写的内容)、输入架构中的参数描述和枚举值,以及只读注释。扫描会检测针对模型的指令(<IMPORTANT>、“不要告诉用户”、“忽略之前的指令”)、跨工具影射(“使用 X 工具时,务必……”)、读取.ssh/.env的请求、隐藏的零宽字符、双向文本字符和 ANSI 字符、含有西里尔字母或希腊字母形似字符的单词,以及与描述相矛盾的readOnlyHint。在block模式下(默认值;未设置时遵循promptInjection.action),存在严重或高危发现的工具不会列入清单,其调用也会被拒绝;中危发现(编码数据块)仅发出警告。每项发现仅记录一次,格式为ruleId: poisoning:<pattern>。这些规则与@guardbee/mcp-tool-poisoning-scanner使用的规则相同,现已通过 guard-core 共享。 - **凭据外泄(
interceptors.egressSecrets,默认开启):**在egress工具运行前,会检查它实际将收到的参数中是否含有凭据——包括提供商密钥(AWS、GitHub、OpenAI、Anthropic、Stripe、Slack 等)、私钥和含密码的连接字符串;仅使用 PII 脱敏所用的固定格式规则,不使用宽泛的关键词规则。数据污点只能识别本次会话的工具返回的内容;此功能还会检测模型从提示词、文件或自身上下文中获取的密钥。block(默认值;未设置时遵循promptInjection.action)会拒绝调用,warn会记录警告;无论哪种模式,审计事件中的凭据都会被脱敏。除非设置piiMasking.detokenizeForEgress,否则 PII token 对 egress 工具仍是 token,只有设置该项后才会检查其值。allowTools可通过 glob 豁免工具(例如接收数据库 URL 的部署工具)。个人数据由 PII 脱敏和污点机制处理。 - **采样(
interceptors.sampling,默认关闭):**服务可以向 Agent 的模型请求补全(sampling/createMessage)——恶意服务可能借此向模型偷偷传递指令或窃取数据。关闭时,代理完全不会向服务提供采样功能。开启时,仅当该服务的某个工具调用正在运行时才会响应请求,并由发起调用的会话处理(两个会话同时调用同一服务时会被拒绝,因为无法确定请求属于哪一个会话);includeContext会强制设为none,因此服务永远无法获取对话内容;maxTokens会设上限(默认 1024),每次调用的请求次数也会设上限(maxPerCall,默认 3);系统提示词和消息都会经过注入扫描;approval: true会先征求用户同意。模型的回答会返回给服务,因此按外泄内容处理:若其中包含本次会话见过的凭据或数据,则会被阻止;个人数据则会被脱敏。事件使用tool: sampling:<upstream>和ruleId: sampling:<check>。代理启动时会提供此功能,因此开启或关闭它都需要重启。 - 审批:
approve(规则操作,或针对有毒数据流设置taint.mode: approve)会通过 MCP elicitation 向用户显示是/否表单,其中包含工具、原因和参数。除明确同意外的任何情况——拒绝、取消、未在approval.timeoutSeconds(默认 120)内作答——都会阻止调用。不支持 elicitation 的客户端无法进行审批,因此调用会被阻止,并显示相应提示。 - **通过仪表板审批:**设置
approval.channels: [elicitation, dashboard]和audit.dashboard后,无法显示提示词的客户端不再会直接阻止调用。请求会发送到 GuardBee 仪表板(MCP Gateway 页面),其中参数会进行 PII 脱敏;工作区所有者和管理员会收到通知,代理会暂停调用,直到有人在仪表板上批准或拒绝,或timeoutSeconds到期。系统会按顺序尝试各渠道;第一个能够发出请求的渠道会给出结果。 - **基于数据的污点(
taint.basis: data):**默认的capability基准会在会话持有不可信内容和敏感内容后阻止所有 egress 调用。使用data时,代理会保存敏感工具(以及含 PII 的结果)返回内容的哈希指纹——个人数据和凭据、类似 ID 的 token,以及连续词语片段——只有当 egress 调用的参数包含这些数据(至少 12 个共同词语,或完全匹配的标识符;PII token 按其值计算),且会话也读取过不可信内容时,才会将其视为有毒数据流。阻止消息会指出数据来源工具。许可证文本、Markdown 模板文本,以及普通结果或至少 3 个工具也返回过的文本,都会被忽略。指纹是哈希值,并按会话保存在内存中。权衡:无法检测对数据的转述或摘要;若这类情况很重要,请继续使用capability。在capability模式下,如果发现相同证据,也会将其添加到阻止原因中。 - **异常检查(
interceptors.anomaly):**检查单次调用中看不出的行为,并在滑动时间窗口内按会话计数。callBurst——60 秒内超过 60 次工具调用(循环,或拒绝服务消耗费用攻击)。sensitiveSweep——300 秒内读取超过 25 个不同的sensitive工具(逐条枚举存储记录;重复读取同一条记录不计入)。destructiveBurst——60 秒内超过 5 次destructive调用。repeatedBlocks——300 秒内超过 5 次调用被阻止或拒绝(探测策略;无法显示审批提示词的客户端不计入)。每项使用{ count, windowSeconds };上述数字均为默认值。warn会记录每项检查首次触发的情况;block会拒绝超出限制的调用,直到时间窗口滑过;若重复触发阻止,则会拒绝该会话之后的所有调用。事件为warn/blocked,并使用ruleId: anomaly:<check>,因此仪表板无需更改即可显示这些事件。除非配置,否则关闭。 - **配额(
quotas):**限制固定时间窗口内的工具调用次数,并跨会话计数——interceptors.anomaly监控单个会话,而用户打开十个会话时,配额仍会生效。每个条目包含match(tool和userglob、upstream、label、group;均为可选项)、limit、windowSeconds和per:user(默认值;OIDC 用户,或在没有用户时使用会话)、session,或gateway(所有人共用一个额度,例如付费上游)。示例:quotas: [{ id: exports, match: { label: egress, group: contractors }, limit: 20, windowSeconds: 3600 }]。每个匹配的配额都会被检查;一次调用要么计入所有配额,要么一个都不计入;只有即将到达上游的调用才会计数,因此被策略拒绝的调用不会消耗配额。配额用尽时会拒绝调用,并告知重试所需时间,记录ruleId: quota:<id>,且不计入anomaly.repeatedBlocks。计数器保存在代理进程中:多个副本各自维护计数。 - 来自仪表板的策略:
policy: { source: dashboard, refreshSeconds: 60 }(需设置audit.dashboard)会从 GuardBee 仪表板中编辑的工作区策略获取标签、规则、污点、审批、默认值和拦截器,而非从此文件获取。上游、监听和审计配置仍由本地管理。代理会在启动时获取策略,并使用 ETag 重新检查;更新会应用于每个会话的下一次调用(标签从下一次tools/list开始生效)。若未保存仪表板策略、无法连接仪表板,或文档无效,代理会继续使用本地策略或上次有效的策略——绝不会在没有策略的情况下运行。 - **文档(
interceptors.documents,默认关闭):**工具结果、嵌入式资源和资源读取中的图像及数据块——PDF、屏幕截图、扫描页面——会使用@guardbee/mcp-document-scanner读取:PDF 文本层,以及在安装可选依赖tesseract.js(扫描版 PDF 还需安装@napi-rs/canvas)后,对图像和没有文本的页面进行 OCR。若文档包含个人数据或凭据,则在block模式下将其替换为提示信息(ruleId: documents:pii),在warn模式下则记录警告;其计数会计入piiHits,并像文本中的 PII 一样污染会话。无法读取的文档(未安装 OCR,或超过maxBytes)会记录为documents:unread。{ enabled: true, action: block, ocr: true, maxBytes: 26214400 }。 - **个人数据计数:**包含 PII 的工具响应和资源读取会附带
piiHits,按类别计数(tc_kimlik、vkn、iban、credit_card、email、phone_tr;凭据计为secret)。只记录数量,绝不记录值;即使关闭脱敏也会计数。仪表板会根据这些数据生成 KVKK 报告。 - **Token 化:**当
interceptors.piiMasking.mode: tokenize时,模型看到的是<pii:tc_kimlik:7f3a9b21>,而不是实际值,并且仍可将它传递给其他工具:代理会在发送给该服务时还原真实值。token 仅在会话期间保存在内存中;除非设置piiMasking.detokenizeForEgress: true,否则不会为egress工具还原为实际值。 - 提示词(
prompts/get)会像工具结果一样进行注入扫描和 PII 脱敏。 interceptors.definitionDrift.recheck: on-change会跳过每次调用时执行的tools/list,仅在服务发送tools/list_changed后重新检查;代理也会将此通知转发给 Agent。- **构建来源验证(
provenance,默认warn):**启动 stdio 上游之前,代理会解析npx/pnpm dlx/bunx将运行的软件包,并检查其 npm 构建来源:Sigstore 签名的 SLSA 证明(根据 Sigstore 信任根验证签名、证书链和透明日志条目,信任根通过 TUF 获取),其声明需指明确切的 tarball(sha512 = 注册表的integrity),由其所指仓库的工作流签名,且该仓库必须与软件包的repository字段匹配。随后,命令会固定到已检查的版本运行(npx -y pkg→npx -y [email protected]),因此之后发布的版本不会被启动。warn会启动未经验证的软件包,并记录ruleId: provenance:unsigned | invalid | error | unsupported;require会拒绝它们(针对各个上游:upstreams.<name>.provenance: off | warn | require)。验证结果按版本缓存在~/.guardbee/provenance.json(provenance.cacheFile)中;此前已验证版本下的其他 tarball 会被视为篡改。provenance.offline: true只使用缓存和磁盘上的信任根(因此需固定确切版本)。无需启动服务即可检查:guardbee-proxy provenance --config <file>(若某个上游将被拒绝,则退出代码为 1)。PyPI(uvx、uv tool run):该版本的每个文件都必须带有其 Trusted Publisher 提供的 PEP 740 证明(由该仓库的工作流签名,并指明文件及其 sha256),且uvx pkg会以uvx pkg==<version>的形式运行。容器映像(docker run、podman run):标签会解析为摘要,并验证映像的 cosign 无密钥签名(sha256-<digest>.sig);对于ghcr.io/<owner>/…,签名者必须是该映像所有者 GitHub 账号中的工作流;其他情况则必须匹配provenance.signers("docker.io/acme/*": "https://github.com/acme/"),且命令会固定到image@sha256:…运行。--format sarif会将发现结果写为供代码扫描使用的 SARIF 2.1.0。限制:许多软件包发布时没有构建来源信息(它们会显示为unsigned;通过其他方式而非 cosign 签名的映像也是如此,例如 Docker Hub 的mcp/映像);私有 npm 注册表通过provenance.registry或npm_config_registry使用,PyPI 镜像通过provenance.pypiIndex使用(需要 integrity API),且不支持私有映像(仅支持匿名注册表 token);单服务--形式不会执行检查。provenance: mode: warn # off | warn | require upstreams: github: command: npx args: ["-y", "@modelcontextprotocol/server-github"] provenance: require # this one must be verified to start - 持久化工具固定(
interceptors.definitionDrift.persist,在网关模式下默认启用): 服务首次列出的定义(名称、描述、输入/输出架构、注释)会通过 SHA-256 哈希固定在~/.guardbee/pins.json中(通过pins.file更改位置;以原子方式写入,权限模式为 0600),这样,即使服务在两次运行之间、更新后或遭到入侵后重写工具,也不会仅仅因为开启了新会话就再次获得信任。定义发生更改时,onChange: approve(默认值)会继续向 Agent 显示已批准的定义,并在首次调用前询问用户,同时并排显示新旧描述;批准后,新定义将被固定。block会拒绝调用,warn会记录日志并使用新定义。已知服务上的新工具也会以相同方式暂缓处理。审计事件(ruleId: pin:changed | pin:added | pin:removed)包含两个哈希;只有启用audit.includePayloads时才会包含描述。使用guardbee-proxy pins list | approve <upstream> [tool] | reset [upstream] [tool]管理固定项,并使用guardbee-proxy pins import <baseline.json> --upstream <name>从 CI 导入初始固定项(mcp-rug-pull-detector基线:哈希相同)。单服务--形式只保留每个会话的固定项。 - 审计日志采用哈希链。默认情况下,它存储参数的 SHA-256 哈希,而不存储参数本身,也不存储结果;设置
audit.includePayloads: true可记录这些内容。使用guardbee-proxy verify-audit ./guardbee-audit.jsonl检查日志。 guardbee-proxy validate --config guardbee-proxy.yaml可在不启动任何服务的情况下检查配置。- 未指定
--config时,代理也会读取./guardbee-proxy.yaml或GUARDBEE_PROXY_CONFIG=<file>.yaml。
上文中的单服务 -- 形式保留其 0.x 行为:工具名称不加前缀,会记录有效负载,而有害流程只会作为警告记录。
沙箱化上游
策略、污染追踪和屏蔽功能可检查 MCP 消息。stdio 服务本身也是一个拥有你权限的进程:遭到入侵的软件包可以读取 ~/.ssh 或 .env 文件并将其发送出去,完全不必响应任何工具调用。upstreams.<name>.sandbox 会改为在 OS 沙箱中运行该服务:
upstreams:
files:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects/demo"]
sandbox:
runtime: sandbox-exec # docker | podman | bwrap | sandbox-exec | none (default: no sandbox)
fs:
readWrite: ["/Users/me/projects/demo"]
readOnly: [] # containers see only these two lists
deny: ["~/clients"] # hidden on top of the built-in credential list
env: ["GITHUB_TOKEN"] # host variables passed in; the upstream's own `env` always is
network: open # open (default) | none | allowlist
# allowHosts: ["api.github.com", "*.githubusercontent.com"] # network: allowlist
limits: { cpus: 1, memory: 512m, pids: 128 } # docker / podman only
所有运行时都会强制执行以下限制:
- 磁盘只读。 只能写入
fs.readWrite和私有主目录~/.guardbee/sandbox/<name>(HOME 和 TMPDIR 指向该目录,因此沙箱服务自己的 npm 或 uv 缓存与您的缓存分开,无法污染您的缓存)。 - 隐藏凭据。
~/.ssh、~/.aws、~/.azure、~/.config/gcloud、~/.kube、~/.docker、~/.gnupg、~/.netrc、~/.npmrc、~/.pypirc、~/.git-credentials、~/.config/gh、密码存储、浏览器配置文件和密钥链、.env文件、代理自身的固定项和来源信息文件,以及fs.deny。 - 不传递主机环境变量。 仅允许
PATH、LANG、LC_ALL、TERM、TZ、env中的名称以及上游的env。 network: none会断开网络(因此必须已安装该软件包:使用包含该软件包的镜像,或先运行open)。network: allowlist配合allowHosts(名称、IP 字面值或*.example.com等 glob 模式)时,只保留一种外联方式:网关中的 HTTP 代理(TLS 使用 CONNECT,普通 HTTP 则转发),且该代理只连接这些主机。连接时会检查地址,因此,如果某个名称解析到云元数据地址、链路本地地址或(除非设置blockPrivate: false)私有或环回地址,就会被拒绝;DNS 重绑定也包括在内。沙箱会获得HTTP_PROXY/HTTPS_PROXY(以及NODE_USE_ENV_PROXY、npm_config_*proxy),上游的env无法覆盖这些变量;忽略这些变量的客户端将无法访问网络,而不是绕过限制。每次拒绝都会生成审计事件(ruleId: network:host | network:metadata | network:private、destination),因此它们会像 HTTP 上游防护器的事件一样进入 SIEM 和 OTel 导出。软件包运行器也会下载内容:对于npx,请列出registry.npmjs.org;对于uvx,请列出pypi.org和files.pythonhosted.org。 | 运行时 | 操作系统 | 文件系统 | 隐藏凭据 | 无网络 | 网络允许列表 | 限制 | | --- | --- | --- | --- | --- | --- | --- | |docker/podman| Linux、macOS、Windows | 仅挂载路径;根目录只读,--cap-drop ALL,no-new-privileges| 在挂载内容中(tmpfs //dev/null) |--network none| 一个仅与转发器 sidecar(node:22-alpine)共享的--internal网络;转发器将请求中继到代理(Linux 上使用 unix socket,Docker Desktop / podman machine 上使用host.docker.internal);退出时会移除这三者 |cpus、memory、pids| |bwrap(bubblewrap) | Linux | 整个磁盘只读,--unshare-all| 在其上覆盖 tmpfs //dev/null| 不共享网络命名空间 | 独立网络命名空间(仅 loopback),并通过转发器将127.0.0.1:3128转发到代理的 unix socket | — | |sandbox-exec(Seatbelt) | macOS | 整个磁盘可读,禁止写入 |deny file-read*(对任意深度的.env文件生效) |deny network*|deny network*,但允许访问代理在 localhost 上使用的端口 | — |
容器使用 image;node/npx/npm/pnpm 使用 node:22-alpine,uvx/uv/python 使用 ghcr.io/astral-sh/uv:python3.12-alpine,bun/bunx 使用 oven/bun:1-alpine。环境变量值通过客户端的环境传入容器(-e NAME),绝不会放在命令行中。在 Linux 上,容器以你的用户身份运行,因此它写入的文件仍归你所有。
- 不会静默回退。 如果沙盒无法在此主机上运行(运行时不在
PATH中、在 Linux 上使用sandbox-exec、没有容器却配置了limits、对~或/设置readWrite、路径位于凭据存储中),网关会停止并说明原因,guardbee-proxy validate也会列出同样的问题。启动失败会记录为审计事件ruleId: sandbox:invalid | sandbox:failed。 guardbee-proxy init --sandbox [runtime]会将每个导入的 stdio 服务放入沙盒(默认:macOS 上使用sandbox-exec,Linux 上若已安装则使用bwrap,否则使用docker),并允许写入传入的目录。guardbee-proxy init --grades会打印每个导入服务的 GuardBee MCP Index 等级(A–F,依据其已发布的软件包评定:静态扫描、已知漏洞和恶意软件、构建来源、维护情况;索引会整体下载并在本地搜索,因此不会发送服务器名称)。--min-grade C会排除等级低于 C 的服务:这些服务既不会被放到代理后面,也不会留在客户端配置中以便不受检查地运行;备份中会保留它们。没有等级的服务(本地命令、远程 URL、官方注册表之外的服务)始终会被迁移。- 模型的局限。 Apple 已弃用
sandbox-exec,但它仍是 macOS 上的沙盒机制;它不限制 CPU 或内存。bwrap需要非特权用户命名空间(某些发行版会通过 AppArmor 限制它们)。在非容器运行时中,两者都不会阻止读取未列出的文件:只有凭据列表、.env文件和fs.deny会被隐藏,其他所有内容仍可读取(如需允许列表,请使用容器)。network: allowlist按主机名过滤,不区分端口:它不会检查发送到允许主机的内容。原始 TCP 和 UDP(数据库驱动程序、DNS)不会通过 HTTP 代理,因此在该模式下无法联网。Windows 没有内置运行时:请使用docker或podman。
上游身份
默认情况下,HTTP 上游会为每个调用方使用 headers 中的凭据。在共享网关中,这意味着所有人都会以同一个下游账户的身份操作。upstreams.<name>.auth 可让每个调用方以自己的身份操作。调用方自己的令牌绝不会转发,因为 MCP 规范禁止令牌透传。
**令牌交换(RFC 8693)。**调用方使用 OIDC(listen.oidc)登录网关。对于每个上游,网关都会在授权服务器处将该令牌交换为受众为该上游的令牌。
upstreams:
files:
url: https://files.internal.example/mcp
auth:
mode: token-exchange
tokenEndpoint: https://sso.example/realms/acme/protocol/openid-connect/token
clientId: guardbee-gateway
clientSecretEnv: GATEWAY_CLIENT_SECRET # never in the file
audience: files-api # RFC 8693 audience; resource: (RFC 8707) also works
scopes: [files:read] # what every call gets
toolScopes: { "delete_*": [files:write] } # step-up for these tools
- Keycloak(26.2+,标准令牌交换):在网关客户端上启用 Standard token exchange。调用方的令牌必须在
aud中包含网关(在 MCP 客户端上配置受众映射器),并且网关客户端需要配置指向上游客户端的受众映射器。CI 测试会在 docker 中运行 Keycloak。 - Microsoft Entra ID(on-behalf-of):将
tokenEndpoint指向https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token。Entra 使用 OBO 授权(urn:ietf:params:oauth:grant-type:jwt-bearer),而非 RFC 8693,因此请在其前面部署 RFC 8693 代理,或使用支持 8693 的提供方。 - Okta(自定义授权服务器,且应用已启用令牌交换):
tokenEndpoint: https://<org>.okta.com/oauth2/<server>/v1/token,audience为目标授权服务器的受众。
**按用户进行 OAuth。**对于第三方服务(GitHub、Google、Atlassian……),每位用户只需连接一次自己的账户。
listen: { transport: http, oidc: { … } }
upstreamAuth:
publicUrl: https://gateway.example # redirect URI: https://gateway.example/oauth/callback
encryptionKeyEnv: GUARDBEE_TOKEN_KEY # openssl rand -base64 32
upstreams:
github:
url: https://api.githubcopilot.com/mcp/
auth:
mode: oauth-per-user
authorizationEndpoint: https://github.com/login/oauth/authorize
tokenEndpoint: https://github.com/login/oauth/access_token
clientId: Iv1.abc123
clientSecretEnv: GITHUB_APP_SECRET
issuer: https://github.com # RFC 9207 iss check, when the server sends it
scopes: [repo]
在用户连接之前,上游会提供一个工具 <name>__connect_account。该工具会返回网关授权页面的链接,页面会显示上游、用户和作用域,并提供 Allow 和 Cancel 选项。选择 Allow 后,页面会跳转到授权服务器,再返回回调地址,届时便会显示实际工具。
整个流程的每一步都有保护:
- 授权链接已签名,与用户绑定,有效期为十分钟,且只能使用一次;
- 表单携带 CSRF 令牌,该令牌也会通过
HttpOnly; SameSite=StrictCookie 设置; - 授权请求使用 PKCE S256 和只能使用一次的
state; - 只有一个精确的重定向 URI;
- 会检查响应中的
iss; - 页面无法被嵌入框架(
frame-ancestors 'none'、X-Frame-Options: DENY),也不会被缓存。
令牌按用户和上游分别存储,并使用 AES-256-GCM 加密(upstreamAuth.tokenFile,默认值为 ~/.guardbee/upstream-tokens.enc),并会在过期时刷新。
**最小化作用域与逐步提升。**调用会请求 scopes,以及被调用工具的 toolScopes。当上游返回 401/403,且包含 WWW-Authenticate: Bearer error="insufficient_scope", scope="…" 时,网关会获取包含这些作用域的令牌,并重试一次。令牌交换会静默完成此操作;按用户进行 OAuth 则会将用户带回授权页面,以请求更大的作用域集合。每次权限提升都会记录为审计事件(type: approval、ruleId: auth:step-up,包含提升前后的作用域),每次授权也会记录审计事件(auth:consent)。
**审计。**此类上游的每个工具响应都会记录 user(网关处的调用方)和 upstreamIdentity(上游看到的身份,即下游令牌中的 preferred_username、upn、email 或 sub)。绝不会记录令牌。
提示词注入分类器(可选)
规则只能捕获已知短语:在 GuardBee 的土耳其语 + 英语基准测试中,可检测出 11%–19% 的攻击。可选分类器 @guardbee/injection-model 是一个小型多语言模型,可在 CPU 上离线运行。它能识别释义、翻译、土耳其语后缀形式和越狱人设,每段文本约需 8 ms。除非进行配置,否则不会启用。
npm i -g @guardbee/injection-model && guardbee-injection-model download # ~135 MB, hash-checked; the only network access
classifier:
# modelDir: /opt/guardbee/injection-model # default: ~/.cache/guardbee/injection-model
# url: http://127.0.0.1:8766 # or the sidecar (guardbee-injection-model serve); tokenEnv for its bearer token
warnAt: 0.5 # at or above: warn
blockAt: 0.9 # at or above: the interceptor's action (block, or warn in warn mode)
timeoutMs: 1000
onError: open # open: keep the rules' decision; closed: block when the classifier fails
apply: [arguments, results, resources, llm] # tool arguments, tool results, resources, LLM gateway prompts
- 规则仍然具有权威性。 分类器只会处理未被规则拦截的内容。它可以添加警告或拦截,但绝不会清除规则的判定。之后仍为警告的内容会在配置了裁判时交由其处理,因此分类器负责快速初筛,裁判提供第二意见。
- 记录在案。 每条由分类器处理过的审计事件都会带有
classifier: { model, score, latencyMs };如果分类器失败,则会带有error。分类器拦截时的ruleId为classifier:injection(在 LLM 网关中为llm:injection-classifier)。CLI 扫描器接受--classifier,并在 SARIF 中报告classifierModel和classifierScore。 - 数据。 在基准测试的留出集上,规则 + 分类器在土耳其语中的 F1 分数为 93.7,在英语中为 91.6,整体误报率为 2.1%;在笔记本电脑 CPU 上,每段文本平均耗时 8 ms / p95 耗时 30 ms。仅使用规则时,分数分别为 28.6 和 20.4;模型卡片中包含完整表格、训练数据和局限性。
- 逐步启用。 先设置
blockAt: 1(仅发出警告),在自己的流量中查看警告后再启用拦截。
LLM 裁判(可选)
注入规则速度快且易于解释,但只能匹配已知短语。可选的裁判会提供模型的第二意见:判断某条指令是否经过释义、间接表达或翻译,或某篇文章是否只是引用了该指令。除非进行配置,否则不会启用;最好使用本地模型运行(Ollama、llama.cpp、vLLM、LM Studio:任何兼容 OpenAI 的端点均可),这样内容不会离开本机。
judge:
endpoint: http://127.0.0.1:11434/v1 # Ollama; a non-local endpoint also needs allowRemote: true (and https)
model: qwen2.5:7b
budgetMs: 500 # time the judge may add to a call; past it, onError decides
onError: open # open: keep the rules' decision; closed: block the borderline item
minConfidence: 0.7
mayClear: false # true: a confident "benign" removes a rules warning (still audited)
scope: borderline # borderline: only what the rules warned about; results: every tool result too
- 只提升,不清除。 裁判只会处理规则未拦截的内容:工具参数或结果中的
warn(设置scope: results时,也包括规则放行的每条结果)。如果裁判有把握地判定为malicious,则会将其提升为配置的操作(interceptors.*.action,通常为block),审计ruleId: judge:injection。其他判定均保留规则的决定。只有在设置mayClear: true时,benign判定才会清除警告;被清除的警告仍会保留在审计日志中。 - 时限。 如果裁判未能在
budgetMs内应答(或发生故障),则会保留警告(onError: open),或拦截有疑点的内容(closed)。规则判定为干净的结果,绝不会因为裁判不可用而被拦截。 - 记录在案。 每条由裁判处理过的审计事件都会带有
judge: { model, version, verdict, confidence, latencyMs, cached, outcome }。CEF 导出会在flexString1中记录模型名称。判定结果会按输入缓存(cacheSize)。 - 数据。 在 GuardBee 的裁判基准测试中(包含 50 个英语和土耳其语注入样本),规则可捕获 16% 的攻击。
qwen2.5:7b可捕获全部攻击且无误报,在笔记本电脑上每次调用约需 1 s;qwen2.5:3b可捕获 96%,耗时 0.7 s。样本集较小,数据可能偏乐观;请参阅其中的注意事项。
安全报告
guardbee-proxy report 会将审计日志整理成定期报告,供安全团队或审计员使用:总数、按类别分组并映射到 OWASP Top 10 for LLM Applications (2025) 和 MITRE ATLAS 的发现、个人数据计数、拦截次数最多的规则和工具,以及哈希链状态。报告中绝不包含载荷——只有计数和记录的原因。
openssl genpkey -algorithm ed25519 -out guardbee-report.key # once; keep it private
openssl pkey -in guardbee-report.key -pubout -out guardbee-report.pub
guardbee-proxy report ./guardbee-audit.jsonl --days 30 --lang tr --key guardbee-report.key --out ekim.html
guardbee-proxy report ./guardbee-audit.jsonl --from 2026-10-01 --to 2026-11-01 --format json --out october.json
guardbee-proxy verify-report ekim.html --pubkey guardbee-report.pub
| 类别 | OWASP LLM Top 10 | MITRE ATLAS |
|---|---|---|
| 参数和提示词中的提示注入 | LLM01 | AML.T0051.000, AML.T0054 |
| 工具结果、资源和提示词中隐藏的指令 | LLM01, LLM05 | AML.T0051.001 |
| 尝试提取系统提示词 | LLM07, LLM01 | AML.T0056 |
| 被投毒的工具定义 | LLM01, LLM03 | AML.T0051.001, AML.T0053 |
| 获批后变更的工具定义(rug pull) | LLM03 | AML.T0010, AML.T0053 |
| 即将外泄的敏感数据或凭据(有害数据流、凭据外传、文档) | LLM02, LLM06 | AML.T0057 |
| 被策略拒绝或暂缓的调用 | LLM06 | AML.T0053 |
| 突发调用、配额和预算 | LLM10 | AML.T0029, AML.T0034 |
- 格式:Markdown(默认)、独立 HTML(浅色和深色)、JSON;
--lang en|tr。 - 完整性:报告包含其规范 JSON 的 SHA-256 值,以及报告周期内最后一条审计事件的哈希,因此与哈希链关联。使用
--key(Ed25519,PEM)时,报告会进行签名并附带公钥。verify-report可检查.json、.html或.md报告;传入--pubkey可固定签名者——仅凭嵌入的密钥只能证明签名后报告未被更改,无法证明签名者身份。 - 哈希链若已损坏,会在报告和 stderr 中说明。
- 映射定义位于
@guardbee/guard-core(classifyEvent、THREAT_CATEGORIES)中,并由@guardbee/mcp-threat-rules的framework_map工具展示。 - **合规框架(
--framework eu-ai-act | iso42001 | soc2 | nist-ai-rmf):**添加一个章节,列出网关支持的框架要求及其引用来源(EU AI Act 条款、ISO/IEC 42001 Annex A 控制项、SOC 2 Trust Services Criterion、NIST AI RMF 子类别),并列出该周期内的相关证据:映射到该要求的类别中的发现,以及实际使用到的网关能力(用于记录保存的哈希链日志、用于人工监督的审批、用于供应商控制的固定值和来源验证……)。该章节包含在签名报告中。这些表格是 GuardBee 对相关文本的解读,在@guardbee/guard-core中按版本管理(mappingVersion,COMPLIANCE_FRAMEWORKS):用于评估的证据,而非认证。
AI 物料清单
guardbee-proxy aibom --config guardbee-proxy.yaml --out aibom.cdx.json 会生成网关置于 Agent 前端的 CycloneDX 1.6 AIBOM:每个 MCP 服务都作为组件列出,并附带其包 URL(pkg:npm、pkg:pypi、pkg:oci)、固定时的版本、构建来源状态、来源缓存中的源代码仓库,以及沙箱设置;每个服务都作为一项服务列出,其工具嵌套其中(名称、描述、定义固定到的 SHA-256、声明的提示);远程服务附带其端点;LLM 网关的路由则作为 machine-learning-model 组件列出,并标明其提供方。该命令会读取配置、固定值文件和来源缓存,不会发出请求;环境变量值、标头和密钥绝不会包含在其中。输出符合官方 CycloneDX 1.6 架构(已测试)。
可观测性(OpenTelemetry)
每次工具调用都是一个名为 tools/call <tool> 的 span(类型为 SERVER),带有 MCP 语义约定属性——mcp.method.name、gen_ai.tool.name、gen_ai.operation.name: execute_tool、mcp.session.id、enduser.id(OIDC)——以及 guardbee.upstream、guardbee.labels 和 guardbee.pii_hits。对上游的调用是一个子 span(类型为 CLIENT,server.address = 上游)。该调用的每个审计事件——拦截、毒性流、审批、警告、服务器的采样请求——都是一个 span 事件(guardbee.blocked 等,带有 guardbee.rule_id 和 guardbee.reason),被拦截的调用带有 guardbee.blocked: true。参数和结果绝不会写入 span。
代理仅依赖 @opentelemetry/api,因此没有 SDK 时不会产生任何开销。若要通过 OTLP/HTTP 导出,请在代理旁安装 SDK 并设置标准环境变量:
npm i -g @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com:4318
export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer …" # if your collector needs it
export OTEL_SERVICE_NAME=guardbee-gateway # default: guardbee-security-proxy
Docker 镜像已包含 SDK。设置 OTEL_SDK_DISABLED=true 或 OTEL_TRACES_EXPORTER=none 可将其关闭;如果某个进程先注册了自己的 SDK(通过 NODE_OPTIONS 进行零代码插桩),则会继续使用该 SDK。
远程模式(HTTP)
代理可以作为共享网络服务运行,而不是作为某个客户端的子进程运行。Agent 通过 Streamable HTTP 连接,上游也可以是远程 Streamable HTTP 服务。
version: 1
listen:
transport: http # stdio (default) | http
host: 0.0.0.0 # 0.0.0.0 in a container; 127.0.0.1 on a laptop
port: 8787
path: /mcp
apiKeys: ["${GUARDBEE_PROXY_KEY}"] # agents send Authorization: Bearer <key>
oidc: # and/or: users sign in with your identity provider
issuer: https://login.acme.com
audience: guardbee-gateway
maxSessions: 100
upstreams:
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
linear:
url: https://mcp.linear.app/mcp # a Streamable HTTP upstream
headers: { Authorization: "Bearer ${LINEAR_TOKEN}" }
network: { mode: allowlist, allowHosts: ["mcp.linear.app"] } # only these hosts, even through a redirect
audit:
sink: file
filePath: /var/log/guardbee/audit.jsonl
dashboard:
url: https://app.guardbee.ai/api/v1/gateway/events
apiKeyEnv: GUARDBEE_API_KEY # workspace API key with the gateway.write scope
-
会话彼此隔离。 每个 MCP 会话都有自己的污点状态、PII 令牌和工具固定项,因此一个 Agent 的毒性流不会拦截另一个 Agent。所有会话共享上游连接。审计事件带有
sessionId。MCP 2026-07-28 没有会话;替代方案请参阅 MCP Protocol Versions。 -
没有密钥,就无法联网。 如果监听地址不是回环地址,且未设置
listen.apiKeys或listen.oidc,则会产生配置错误。密钥采用恒定时间比较,错误或缺失的密钥会收到401。 -
网络出口(
upstreams.<name>.network): 代理向 HTTP 上游发起的每个连接(包括每次重定向跳转)都会在连接时进行检查:解析主机名、检查每个地址,并让套接字连接到经过检查的地址,因此 DNS 答案在检查和连接之间发生变化(重绑定)时,无法访问内部主机。任何编码形式的链路本地地址(例如169.254.169.254这样的云元数据地址)、0.0.0.0和保留地址段都会被拒绝(包括八进制/十六进制 IPv4、IPv4 映射或 NAT64 IPv6)。blockPrivate会拒绝回环地址和私有地址(RFC 1918、CGNAT、fc00::/7);未设置时,则遵循已配置的 URL:localhost或私有地址上的服务可以保持在本地,但公网上的服务不能访问你的网络。mode: allowlist配合allowHosts(*.example.com通配符)可限制主机。被拒绝的连接会生成审计事件type: blocked,其中包含ruleId: network:private | network:metadata | network:reserved | network:host和destination(主机和地址,不包含请求内容),因此这些事件会发送到 SIEM 导出器和仪表板。Stdio 服务会自行打开套接字;如需限制这些连接,需要使用 OS 沙箱,此处不作介绍。 -
目录组(
listen.ldap): LDAP / Active Directory 组会添加到调用方已有的组中——OIDC 用户或 LLM 网关密钥对应的用户——因此规则、配额、llm.access和预算中的group匹配可以使用组织已有的组。目录不会让调用方获得身份认证:认证仍由 API 密钥、OIDC 或网关密钥负责。listen: ldap: url: ldaps://dc.acme.com # certificates verified; caFile for a private CA bindDn: "CN=svc-guardbee,OU=Service,DC=acme,DC=com" bindPasswordEnv: LDAP_BIND_PASSWORD baseDn: "DC=acme,DC=com" userFilter: "(|(mail={user})(userPrincipalName={user}))" # {user} is escaped (RFC 4515) groupAttribute: memberOf # AD; or groupSearch: { baseDn, filter: "(member={dn})", nameAttribute: cn } groupName: cn # CN=Platform Eng,OU=… → "Platform Eng"; dn keeps the DN cacheSeconds: 300 onError: deny # directory down: 503 (default) | token-groups: go on without itSAML 不是代理设置:MCP 和大模型端点使用 bearer 令牌进行身份验证,而支持 SAML 的身份提供方(Entra ID、Okta、ADFS、Keycloak)也会签发 OIDC 令牌——请搭配使用
listen.oidc,对于令牌中不包含的组,则使用listen.ldap。 -
**用户(
listen.oidc):**代理会接受来自 OpenID Connect 提供方(Okta、Entra ID、Auth0、Keycloak、Google……)的 JWT,可与 API 密钥并用,也可取代 API 密钥。它会根据签发方的 JWKS(通过其发现文档或jwksUri获取)验证签名、签发方、audience(必需:拒绝为其他服务签发的令牌)和有效期。用户信息来自userClaim(默认值为sub;常用值为email),组信息来自groupsClaim(默认值为groups,可以是列表或以空格分隔的字符串)。规则可以匹配user(glob,例如"*@contractor.com")和group,且每条审计事件都包含user。会话属于创建它的用户:其他用户的令牌访问该会话时会收到403。401响应包含WWW-Authenticate: Bearer resource_metadata=…,而/.well-known/oauth-protected-resource(RFC 9728)会指明签发方,因此实现了授权规范的 MCP 客户端可以找到登录入口;在反向代理后运行时,将oidc.resource设置为公开 URL。 -
**SIEM 导出(
audit.exporters):**每条审计事件还会以批处理方式在请求处理路径之外发送到 Splunk、Elasticsearch/OpenSearch 或 syslog 收集器。凭据从环境变量读取;事件与本地日志中的事件相同,均已哈希且不含载荷(除非设置了includePayloads)。audit: exporters: - { type: splunk, url: "https://splunk:8088/services/collector/event", tokenEnv: SPLUNK_HEC_TOKEN, index: security } - { type: elastic, url: "https://es:9200", index: guardbee-audit, apiKeyEnv: ES_API_KEY } # or usernameEnv / passwordEnv - { type: syslog, host: siem.acme.internal, port: 6514, protocol: tls, format: cef, caFile: /etc/ssl/acme-ca.pem }Splunk 接收 HEC 事件(
sourcetype: guardbee:audit)。Elasticsearch 接收_bulkcreate操作,使用事件哈希作为_id,因此重试的批次不会造成重复(409视为已发送),并包含@timestamp。Syslog 使用 UDP、TCP 或 TLS 上的 RFC 5424(始终验证证书;私有 CA 使用caFile),支持八位字节计数(默认)或换行分帧,默认设施码为 13(日志审计),消息格式为 CEF(默认)或 JSON。CEF 会将act、suser、rt、msg和tool/upstream/ruleId/sessionId/piiHits/labels放入cs1–cs6;blocked和toxic_flow的严重性为 8,warn和approval为 5,其他情况为 3。每个目标都有独立队列(最多 10,000 条事件):SIEM 宕机时,事件会稍后发送;SIEM 拒绝事件(401、映射错误)时,会在 stderr 上报告一次。 -
**多个副本(
state):**默认情况下,配额和大模型预算在进程内计数,因此负载均衡器后的每个副本都允许使用完整额度。设置state.backend: redis后,所有副本都会计入相同的额度:检查和计数通过单个 Lua 脚本运行,因此不会有多个副本同时占用最后一次调用,且键会随其窗口过期(Redis 6 或更高版本;键使用相同的哈希标签,因此 Redis Cluster 也可运行)。代理自行使用 RESP 通信:无需安装任何组件。如果无法连接 Redis,则由onError决定处理方式:local(默认值)会在 Redis 恢复前于进程内计数,fail-open会放行调用但不计数,fail-closed会拒绝受配额或预算限制的调用;服务中断和恢复都会记录为审计事件(ruleId: state:unavailable | state:recovered)。异常窗口仍按会话分别计算,这本来就是每个副本各自维护的:MCP 会话只会运行在一个副本上。state: backend: redis # memory (default) | redis url: rediss://:${REDIS_PASSWORD}@redis.internal:6380/0 # rediss:// = TLS, certificates verified; /0 = database keyPrefix: guardbee # keys: {guardbee}:quota:… and {guardbee}:budget:… onError: local # local | fail-open | fail-closed timeoutMs: 250 -
GET /healthz返回{"ok": true, "sessions": N, "contexts": M}(会话自 2025 年起使用,contexts 自 2026-07-28 起使用)。超过maxSessions时,会话和 contexts 的请求会收到503。 -
仪表板:
audit.dashboard会将每条审计事件以批处理方式发送到 GuardBee 仪表板,使用从apiKeyEnv获取的工作区 API 密钥。密钥不会保存在文件中。如果无法访问仪表板,事件会暂存在内存中(最多 10,000 条),稍后再发送。密钥被拒绝时,会在 stderr 上报告一次。工具调用不会等待仪表板响应。 -
init现在也会将 Streamable HTTP 服务(url+headers)移至代理后方。Legacy SSE 服务仍保留在客户端配置中,并会报告出来。
Docker:
docker build -t guardbee/mcp-security-proxy packages/security-proxy
docker run -p 8787:8787 -e GUARDBEE_PROXY_KEY=... -e GUARDBEE_API_KEY=... \
-v "$PWD/guardbee-proxy.yaml:/etc/guardbee/guardbee-proxy.yaml:ro" guardbee/mcp-security-proxy
MCP 协议版本
代理在两端都支持 MCP 2026-07-28 和 2025-11-25(以及更早的 2025 修订版),并能混合使用:2025 时代的 Agent 可以通过代理使用 2026-07-28 服务,反之亦然。它基于 MCP TypeScript SDK v2 构建。
-
Agent。 通过 stdio 连接时,客户端连接后即可确定协议修订版。通过 HTTP 连接时,每个请求都会声明所用的协议修订版:2025 时代的客户端会像以前一样获得一个会话(
Mcp-Session-Id),而 2026-07-28 请求无需会话即可处理。 -
上游服务。 默认情况下,代理会向每个服务请求 2026-07-28(
server/discover),并在失败时回退到 2025 的initialize握手。可通过protocol为每个上游服务固定协议修订版:upstreams: linear: url: https://mcp.linear.app/mcp protocol: auto # default | 2025-11-25 (no probe) | 2026-07-28 (modern only)对于 stdio,
auto会短暂地再启动一次服务以探测其协议;对于不得启动两次的服务,请设置protocol: 2025-11-25。在docker或podman沙箱中运行的服务默认使用2025-11-25;设置为auto或2026-07-28时,会在服务自己的容器中进行探测。 -
会话的替代机制(HTTP,2026-07-28)。 污染状态、PII 令牌、工具固定项和异常计数器按用户和上下文保存:用户来自 OIDC 令牌(使用 API 密钥的调用方都算作同一用户),上下文来自可选的
X-GuardBee-Context标头(1–128 个字母、数字、.、_、:、-),例如每个 Agent 对话使用一个值。若没有此标头,同一用户的调用会共用一个上下文,这种设计倾向于阻止操作:一个对话读取的数据,另一个对话尝试发送时仍会被计入。若listen.contextIdleSeconds(默认值为 1800)时间内没有请求,上下文就会被丢弃;会话和上下文的总数上限由listen.maxSessions控制。发现请求和subscriptions/listen请求(根据Mcp-Method标头识别)不会使用上下文。 -
审批。 2026-07-28 服务无法在调用过程中向客户端发送 elicitation 请求。当规则要求在调用前审批时,代理会以
input_required响应调用;客户端显示表单,并在用户回答后重新发送调用,只有此时调用才会到达服务。答案会放在经过签名的requestState中,其中绑定了用户、工具和确切的参数,因此无法将其用于其他调用或伪造。只能在服务响应后发起的审批(针对结果的主题规则)以及 sampling 审批,会在approval.channels配置了频道时发送到该频道,否则会被拒绝。在负载均衡器后运行时,请为每个副本提供相同的密钥,以便任何副本都能验证答案:listen: stateKeyEnv: GUARDBEE_STATE_KEY # at least 32 bytes; default: a random key per process -
Sampling。 2026-07-28 客户端无法在另一个服务调用过程中被请求生成补全,因此使用此类客户端时,服务的 sampling 请求会被拒绝(
sampling:unavailable)。若 Agent 使用 2025-11-25,发起调用期间请求 sampling 的 2026-07-28 服务仍按以前的方式处理。 -
不支持 SSE。 代理仅连接和提供 Streamable HTTP 服务。
guardbee-proxy init会排除 SSE 服务,并将其指向大多数服务现在使用的/mcp端点,而不是/sse。
大模型 API 网关
在 HTTP 模式下,代理还可以充当应用与模型提供商之间的中间层。应用将 SDK 的基础 URL 指向代理,并使用网关密钥;真正的提供商密钥保存在代理的环境中,不会泄露到代码、笔记本电脑或 CI 中。
version: 1
listen: { transport: http, host: 0.0.0.0, port: 8787 }
upstreams: {} # MCP servers are optional when llm is set
llm:
path: /v1 # POST /chat/completions, /responses, /embeddings, /messages; GET /models
providers:
openai: { api: openai, baseUrl: https://api.openai.com/v1, apiKeyEnv: OPENAI_API_KEY }
anthropic: { api: anthropic, baseUrl: https://api.anthropic.com, apiKeyEnv: ANTHROPIC_API_KEY }
local: { api: openai, baseUrl: http://ollama:11434/v1, apiKeyEnv: OLLAMA_KEY }
routes: # first match wins; a request only goes to a provider of its own API
- { model: "gpt-*", provider: openai }
- { model: "claude-*", provider: anthropic }
- { model: "llama*", provider: local }
- { model: "gpt-4o-internal", provider: openai, masking: off } # explicit passthrough, audited
keys: # gateway keys, one per person or app (values from the environment)
- { keyEnv: GB_KEY_ALICE, user: [email protected], groups: [eng] }
- { keyEnv: GB_KEY_BILLING, user: billing-svc, groups: [apps] }
pricing: # USD per 1M tokens, for cost in the audit log and costUsd budgets
"gpt-4o": { input: 2.5, output: 10 }
"claude-*": { input: 3, output: 15 }
budgets:
- { id: per-user-daily, per: user, windowSeconds: 86400, costUsd: 20 }
- { id: contractors, per: user, windowSeconds: 3600, requests: 100, match: { group: contractors } }
- { id: team-tokens, per: gateway, windowSeconds: 86400, tokens: 5000000, match: { model: "gpt-*" } }
access: # which models a caller may ask for; matching entries add up
- { group: contractors, allow: ["gpt-4o-mini"] }
- { user: "*@acme.com", allow: ["gpt-*", "claude-*", "llama*"] }
accessDefault: allow # for callers no entry matches: allow | deny
sensitive: # prompts with personal data go to your own model instead
model: llama3.1 # needs a route for the request's API, or the request is refused
on: { pii: true, categories: [tc_kimlik, iban, passport], topics: [ma] }
mask: false # the data stays in-house, so it is not masked (true: keep masking)
maskResponses: false # also mask PII the model writes (prompts are always checked)
timeoutSeconds: 120
maxTextChars: 2000000 # longest single text the proxy masks; longer → 413
documents: # read inline PDFs, images and audio instead of refusing PDFs/audio (415) and passing images; off by default
pdf: true
images: true # OCR inline images (needs tesseract.js)
ocr: false # true: OCR PDF pages with no text layer (needs tesseract.js + @napi-rs/canvas)
languages: [tur, eng] # Tesseract languages
# langPath: /opt/tessdata # local <lang>.traineddata, so OCR never downloads language data
# audio: # transcribe input_audio with your own OpenAI-compatible speech-to-text service
# url: http://127.0.0.1:8080/v1/audio/transcriptions
# model: whisper-1 # sent as the form's model field
# language: tr # optional hint; unset: detected
# apiKeyEnv: STT_API_KEY # optional, sent as Authorization: Bearer
maxBytes: 20971520 # per file; larger → 415
maxPages: 50 # a longer PDF → 415
timeoutSeconds: 30 # reading time per file; over it → 415
interceptors:
piiMasking: { enabled: true, mode: tokenize }
from openai import OpenAI
client = OpenAI(base_url="https://guardbee.acme.internal/v1", api_key=os.environ["GB_KEY"])
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://guardbee.acme.internal", apiKey: process.env.GB_KEY });
每个请求依次经过以下环节:
- 密钥。 OpenAI SDK 使用
Authorization: Bearer,Anthropic SDK 使用x-api-key。llm.keys中的网关密钥会提供用户和组信息;此外,也接受listen.apiKeys和listen.oidc令牌。当设置了llm.keys,且listen未配置密钥时,未知密钥一律会被拒绝,即使请求来自环回地址也是如此。 - 路由和访问权限。 模型名称决定使用哪个提供方;若没有模型路由到某个提供方,则返回
404。llm.access决定调用者可以请求哪些模型:每个匹配用户(通配符)或组的条目都会添加其模型通配符;若没有匹配条目,则应用accessDefault。被拒绝的模型会返回403(ruleId: llm:access),且GET /models只会列出该调用者可用的模型。 - 提示词注入(
interceptors.promptInjection)会检查用户消息和工具消息。应用自身的系统提示词和模型此前的轮次不会被扫描。 - 凭据(
interceptors.egressSecrets)会检查提示词中的任何位置:提供方密钥、私钥和连接字符串会在到达提供方之前被拦截(403)。 - 敏感信息路由(
llm.sensitive)。当提示词包含个人数据(可选地仅检查某些categories)或列出的某个topics时,请求会路由到sensitive.model,即你运行的 Ollama、vLLM 或 llama.cpp 模型,而非请求中指定的模型。审计日志会记录一条warn事件,包含ruleId: llm:route:sensitive、两个模型名称及路由原因,因此路由后的请求会显示在仪表板中;使用情况事件还会包含llm.routedFrom。如果本地模型没有适用于该请求 API 的路由(例如 Anthropic 请求仅有兼容 OpenAI 的本地模型),请求会被拒绝,而不会发送到原本要避开的提供方。预算按路由后的模型计算。 - 预算。 按固定时间窗口统计每个用户(或密钥)或整个网关的请求数、令牌数和 USD 金额。只要每个匹配的预算都有余量,就会允许请求;令牌数和费用只有在回答生成后才能确定,因此触及上限的调用会完成,下一次调用则会收到带有
Retry-After的429。 - PII(
interceptors.piiMasking)会对消息文本、工具调用参数和工具结果进行屏蔽;或者在mode: tokenize时将其替换为令牌,代理会在回答中将这些令牌还原为真实值——提供方看不到这些值,但用户可以。Anthropicdocument块中,来源为文本或内容的文本也会被屏蔽。图像数据、ID 和工具名称不会处理;代理无法读取的内容会被拒绝(参见故障处理)。 - 审计。 记录一条
tool_call事件(包含tool: llm:<model>、upstream: <provider>、labels: [llm]、user、piiHits,以及已读取的 PDF、图像和音频对应的documents——见下文)和一条包含llm: { model, inputTokens, outputTokens, costUsd }的tool_response事件。拒绝请求会记录为blocked事件,ruleId为llm:injection、llm:credential或预算 ID。
端点。 POST /chat/completions、POST /responses(OpenAI Responses API)和 POST /embeddings 会路由到 api: openai 的提供方;POST /messages 会路由到 api: anthropic 的提供方;GET /models 会列出调用者可用的字面路由(非通配符)。Responses 的处理步骤与 chat 相同:instructions、消息项(input_text、此前的 output_text)、函数和自定义工具调用参数、工具调用输出以及存储提示词的 variables 都会被屏蔽;推理项、项引用和提供方自身的工具调用会原样返回;input_file 和 input_audio 会被拒绝(415)。在 mode: tokenize 下,令牌会在回答中还原;但使用 previous_response_id 继续的对话包含此前请求生成的令牌,本次请求无法还原这些令牌——如果回答必须显示真实值,请在 input 中发送历史记录。
流式传输适用于 chat、Messages 和 Responses。普通流会逐个事件中继;对于 OpenAI,代理会添加 stream_options.include_usage 来统计 Token,并在客户端未请求该额外数据块时将其丢弃。当答案必须改写时(恢复 PII Token,或启用 maskResponses),代理会在流式传输过程中改写:文本——包括消息内容、工具调用参数、Anthropic 文本、JSON 和思考增量——只会暂存到安全截断点(两侧都没有数字的空格,因此 IBAN 或银行卡号等分组数字不会被拆开;私钥块会暂存到其 END 行;带有 Adres: 标签的行会暂存到行结束),并与之前已发送的最后 300 个字符一起改写,这样即使值出现在下一个事件中,Pasaport No: 这样的标签仍然适用。仍在暂存的内容会在选项的结束数据块或块的停止事件之前发出。对于 Responses API,output_text、函数调用参数和推理摘要增量也会以相同方式暂存并改写;每个 .done 事件、项目、部分内容以及最终的 response.completed 都会包含相同的改写文本,因此 SDK 组装出的答案与增量一致。提供方错误会原样传递,因此 SDK 重试机制和错误类型都能正常工作。
请求中的 PDF、图像和音频
启用 llm.documents.pdf: true 后,代理会读取内嵌 PDF,而不是拒绝请求:包括消息、系统提示词和工具结果中的 OpenAI file 部分及带有 data:application/pdf;base64, URL 的 Responses input_file 部分,以及带有 Base64 PDF 源的 Anthropic document 块。代理使用 @guardbee/mcp-document-scanner 提取 PDF 的文本层,并在后续每个步骤中用这些文本替代 PDF 进行处理:提示词注入、凭据、主题规则、敏感信息路由和 PII。
-
**无问题:**PDF 会原样发送给提供方。
-
**代理会进行屏蔽的个人数据:**请求会被拒绝并返回
403(ruleId: pii:document),因为代理无法改写 PDF,也不会发送未屏蔽的 PDF。消息会指出数据类别,但绝不会包含具体值。在masking: off的路由上,或llm.sensitive将请求路由到你自己的模型时,PDF 会被转发,其 PII 会计入piiHits。启用llm.sensitive时,包含个人数据的 PDF 会准确地被路由到内部处理。 -
**注入、凭据、主题规则:**与文本相同的拒绝行为(
llm:injection、llm:credential、对应规则的 ID)。 -
**无法完整读取:**超过
maxBytes或maxPages,并非真正的 PDF,已加密或损坏,处理时间超过timeoutSeconds,或者ocr关闭时页面没有文本层。warn事件documents:unread会说明原因;当屏蔽规则适用时,此 PDF 仍无法屏蔽:返回415 guardbee_unmaskable_content(见失败行为)。只读到部分内容的 PDF 绝不会被视为已检查。 -
审计:
tool_call事件的input会包含[pdf "<name>": <bytes> bytes, sha256 <hash>],而不是 PDF。其documents字段会列出读取的每个 PDF:{ name, modality: "pdf", bytes, sha256, pages, chars, checks },其中checks会列出针对其文本运行的检查(injection、credentials、topics、sensitive-routing、pii)。 启用llm.documents.images: true后,内嵌图片也会以相同方式进行 OCR 读取:OpenAI 的image_url和 Responses 的input_image部分(带有data:image/...;base64,URL),以及 Anthropic 的image块(带有 base64 来源),无论它们位于消息、系统提示词还是工具结果中。OCR 识别出的文本会在每项检查中代替图片,处理结果与 PDF 相同:干净的图片会原样转发;包含代理会屏蔽的个人数据的图片会被拒绝,并返回403 pii:document;无法读取的图片(未安装tesseract.js、数据不是图片、超过maxBytes、耗时超过timeoutSeconds)会在应用屏蔽时返回415 guardbee_unmaskable_content。审计占位符为[image "image <n>": <bytes> bytes, sha256 <hash>],documents条目的modality为"image"。 -
安装
tesseract.js,并将其放在代理旁边(npm i tesseract.js)。语言数据(languages,默认为土耳其语和英语)会下载一次并存入~/.cache/guardbee/tessdata;将langPath设为包含.traineddata文件的文件夹,即可让代理保持离线。 -
OCR 读取的是印刷文本,而不是模型能看到的所有内容。 手写文字、低对比度或很小的文字、照片和图表中的文字都可能被漏掉,而模型仍可能读出 OCR 未识别的内容。请将
images: true视为一项用于捕捉文档、聊天和表单截图的检查,而非图片不含个人数据的保证。 -
开销: 笔记本电脑 CPU 处理每张截图约需 0.3 秒,每个包含图片的请求都会处理;图片越大,耗时越长。
timeoutSeconds和maxBytes可限制处理时间与大小。
启用 llm.documents.audio 后,Chat Completions 的 input_audio 部分(wav、mp3)会交由你指定的语音转文本服务进行转录,转录文本会经过所有检查,处理结果相同:干净的音频会原样转发;包含口述个人数据的音频会被拒绝,并返回 403 pii:document;无法转录的音频(其他格式、数据与该格式不符、服务不可用、服务返回错误或耗时超过 timeoutSeconds)会在应用屏蔽时返回 415 guardbee_unmaskable_content。审计占位符为 [audio "audio <n>": <bytes> bytes, sha256 <hash>];转录文本本身不会记录到日志中。
- 服务可以是任何接受 OpenAI 的
POST /v1/audio/transcriptions(multipartfile、model、response_format=json、可选的language)并返回{ "text": … }的端点:whisper.cpp 的whisper-server --inference-path /v1/audio/transcriptions、faster-whisper-server / Speaches、vLLM,或服务提供商的 API。 - 音频会在任何检查之前原样发送到该服务,因此请在允许数据发送到的环境中运行服务:自己的机器或网络。若指向公共 API,每条语音消息都会发送到那里。
- 语音转文本可能会听错。 大声读出的数字(“on bir, sıfır sıfır…”)可能会被转成单词或出现错误,因此请将其视为用于捕捉清晰语音的检查,而非保证。
- 开销: 每个音频部分会转录一次;在 CPU 上,每分钟语音需几秒。
timeoutSeconds和maxBytes可限制处理时间与大小。
尚未覆盖:通过文件 ID 或 URL 提供的 PDF(仍返回 415)、通过 URL 或文件 ID 提供的图片(代理不会获取图片,会将其原样转发)、由 ner: 模型在 PDF、图片或转录文本中识别的人名,以及 Responses 或 Realtime API 中的音频。每个包含 PDF 的请求都会产生 PDF 读取开销;timeoutSeconds 和 maxPages 可限制处理时间和页数。
嵌入
POST /embeddings 会在嵌入每段输入文本前对其进行脱敏;返回的向量保持不变。索引脱敏文本前需要了解以下事项:
- 此处的脱敏是确定性的。 内置值会使用固定占位符(
[TC-KİMLİK]、***@[EMAIL]),不会使用mode: tokenize(令牌按请求生成,因此同一文档每次嵌入的结果都会不同),组织屏蔽模式则按文本编号,而非按请求编号。无论文本在哪个批次中发送,相同文本始终会生成相同向量。 - 仅脱敏值不同的文档会生成相同嵌入。 仅姓名或身份证号不同的两条记录会映射到同一个向量,因此相似度搜索无法区分这些人。请使用自己的 ID 字段(元数据)进行筛选,而不要依赖向量相似度。
- 索引和查询必须使用相同路径。 由脱敏文本生成的向量只能与其他由脱敏文本生成的向量进行比较:请将要索引的文档和要搜索的查询都通过网关发送,并使用相同的
piiMasking设置。 - 长度限制针对每个元素。
input中的每个元素都会根据llm.maxTextChars进行检查;任何一个元素超出限制,整个批次都会被拒绝,并返回413。令牌 ID 输入([[1, 2, 3]])无法脱敏,会被拒绝并返回415——请发送文本。 - 嵌入不会应用提示词注入扫描(关于注入的文档仍必须可以被索引)和
llm.sensitive路由(其他模型生成的向量无法与您的索引进行比较)。凭证、主题、预算和访问检查照常适用。
失败行为
网关采用故障关闭策略:无法检查或遮蔽请求时,不会调用提供方。拒绝响应会在错误正文(error.code,采用 OpenAI 和 Anthropic 的错误格式)以及 x-guardbee-error 标头中带有稳定的代码,并向审计日志写入 blocked 事件。响应和审计日志中都不会保存无法遮蔽的文本——检测器自身的错误消息会替换为其错误类别。
| 状态 | 代码 | 发生情况 | 审计 ruleId |
|---|---|---|---|
502 | guardbee_masking_failed | 检测器或扫描过程抛出错误、阻止模式超出时间预算,或名称模型(ner:)未响应。涵盖调用前的所有检查:注入、凭据、主题、敏感路由、遮蔽。 | pii:masking-failed、pii:pattern-timeout、pii:ner-failed |
415 | guardbee_unmaskable_content | 请求中包含代理无法读取的内容:OpenAI input_audio 或 file 部分;来源为 PDF(base64、url 或 file)的 Anthropic document;未知的部分或块类型;或非文本、非列表内容。启用 llm.documents.pdf 时,代理会读取内嵌 PDF,只有无法完整读取的 PDF 会被拒绝;启用 llm.documents.images 和 llm.documents.audio 时,无法读取的内嵌图片或音频部分也会被拒绝。消息会指出发现的内容。 | pii:unmaskable |
413 | guardbee_text_too_large | 单段文本(消息、部分、工具结果)长度超过 llm.maxTextChars(默认值为 2,000,000 个字符)。限制针对每段文本,而不是整个请求:请将文本拆分为多条消息或多个部分。 | pii:text-too-large |
不会被拒绝的情况:图片(image_url、input_image、Anthropic image)会原样转发;未启用 llm.documents.images 时,不会检查这些图片。在助手轮次中,提供方自身生成的块(thinking、redacted_thinking、server_tool_use、*_tool_result)会原样传回。415 和 413 仅适用于经过遮蔽的请求——当 piiMasking.enabled: false、路由设置了 masking: off,或请求由 llm.sensitive 在内部处理且未设置 mask: true 时,内容不会被遮蔽,也不会因无法遮蔽而遭到拒绝。
无法遮蔽的回答(maskResponses,或还原令牌时发生故障)会被隐藏:文本会被替换为 [GuardBee: withheld, …],流式响应中途也同样如此;事件会使用相同的 ruleId 记录。
按路由直通。 在 llm.routes 条目中设置 masking: off,该路由的提示词(及回答)就会未经遮蔽地发送。必须显式写出此设置——不会默认直通;每个此类请求都会写入一个 warn 事件,其中 ruleId: pii:masking-off 会注明路由;PII 仍会计入 piiHits。在该路由上,动作为 mask 的主题规则仍会执行遮蔽。
在 MCP 网关中,遮蔽失败会隐藏工具结果:客户端会收到 MCP 错误(InternalError、Result withheld: …),其中包含 data: { guardbee: "guardbee_masking_failed" },且不会写入 tool_response 事件。
A2A 网关
多 Agent 系统通过 A2A(Agent2Agent)相互调用。添加 a2a: 配置段后,网关便会位于它们之间。Agent 调用 http://gateway:8787/a2a/<name>,而不是另一个 Agent 自己的 URL;发现 Agent 的客户端可以从 /a2a/<name>/.well-known/agent-card.json 开始。网关会提供 Agent 卡片,并将其中的 URL 改写为指向网关,同时使用网关的身份验证方案。
listen: { transport: http, host: 0.0.0.0, port: 8787, apiKeys: ["${GB_AGENT_KEY}"] } # or listen.oidc
upstreams: {}
a2a:
agents:
research: { url: "http://research-agent.internal:9999/", headers: { authorization: "Bearer ${RESEARCH_AGENT_TOKEN}" } }
billing: { url: "https://billing-agent.internal/a2a", allowGroups: [finance] }
injection: block # or warn
maskRequests: true # personal data masked before it reaches an agent
maskResponses: false
pushAllowHosts: [] # private webhook receivers you run yourself
网关执行以下策略:
| 谁可以调用 | 每个调用方都必须通过 listen.apiKeys 或 listen.oidc 进行身份验证;A2A 不支持开放模式。只能访问已配置的 Agent,且 allowGroups 会将 Agent 的访问权限限制为属于相应组的调用方。Agent 名称只会显示给经过身份验证的调用方。调用方的令牌绝不会被转发:Agent 收到的是为其配置的 headers。 |
| 发往 Agent 的消息 | 检查提示词注入:block 会以 JSON-RPC 错误拒绝请求,warn 会记录日志。文本和数据部分中的个人数据会被遮蔽(maskRequests)。 |
| Agent 返回的内容 | 检查任务、产物、状态消息和历史记录中的间接注入。对流式响应(SendStreamingMessage、SubscribeToTask)逐个事件进行检查;如果发现不良事件,流会以错误结束,且该事件不会被转发。 |
| 推送通知 Webhook | 会解析远程 Agent 将要 POST 到的 URL。如果 URL 指向链路本地或元数据地址(169.254.169.254),则始终拒绝请求。除非主机位于 pushAllowHosts 中,否则也会拒绝指向私有地址和回环地址的请求。此策略适用于 CreateTaskPushNotificationConfig、0.3 版的 tasks/pushNotificationConfig/set,以及 SendMessage 中的推送配置。 |
| 审计 | 每次调用都会生成 tool_call / tool_response 事件(tool: a2a.SendMessage、upstream: <agent>,调用方作为 user),并在 piiHits 中记录遮蔽数量。拒绝请求会生成 ruleId 为 a2a:* 的 blocked 事件。这些事件会发送到与 MCP 流量相同的接收端、SIEM 导出器和 OTel 跟踪中。 |
该功能适用于 A2A 1.0(SendMessage、GetTask 等)和 0.3(message/send、tasks/get 等)的 JSON-RPC 绑定。不会代理 REST 和 gRPC 绑定,改写后的卡片只会列出 JSON-RPC 接口。原始卡片的签名不覆盖改写后的卡片,因此会将其移除。Agent 卡片和 Webhook 配置的静态检查见 @guardbee/mcp-a2a-auditor。完整示例:examples/proxy/a2a.yaml。
编码 Agent 钩子
代理只能看到 MCP 流量。编码 Agent 自己的工具——shell、文件读取、网页抓取——不会经过代理,因此代理看不到先执行 cat .env 再执行 curl -d @.env … 的操作。guardbee-hook 通过各 Agent 的钩子系统,将相同策略应用于这些工具。
npm i -g @guardbee/mcp-security-proxy
guardbee-hook install --claude-code # ~/.claude/settings.json (PreToolUse + PostToolUse)
guardbee-hook install --cursor # ~/.cursor/hooks.json (shell, MCP, file reads)
guardbee-hook install --codex # ~/.codex/hooks.json (PreToolUse + PostToolUse; trust them under /hooks)
guardbee-hook install --gemini-cli # ~/.gemini/settings.json (BeforeTool + AfterTool; turns hooks on)
guardbee-hook install --windsurf # ~/.codeium/windsurf/hooks.json (commands, file reads/writes, MCP)
guardbee-hook install --claude-code --project # .claude/settings.json in this repo
guardbee-hook install --cursor --config ~/.guardbee/guardbee-proxy.yaml --fail-closed
guardbee-hook uninstall --claude-code # removes only GuardBee's entries
安装时会保留其他所有设置和钩子,再次运行时会替换自身条目,并留下 .guardbee-backup 副本。--dry-run 会打印结果而不写入。请从全局安装中运行它:npx 缓存中的路径可能会被清除。
标签。 内置工具会根据其功能获得与 MCP 工具相同的四种标签:
| 工具 | 标签 |
|---|---|
对 .env*、~/.ssh/、id_rsa、~/.aws/credentials、.kube/config、.npmrc、.netrc、*.pem、*.key、*.tfstate、credentials*.json 等执行 Read/Grep/Glob | sensitive |
使用 Bash 读取这些文件、执行 printenv、密钥链和云令牌相关命令 | sensitive |
使用 Bash 执行 curl、wget、nc、scp、ssh、git push、npm publish、docker push、gh api、aws s3 cp 等 | egress(curl/wget/git clone 也会标记为 untrusted) |
使用 Bash 执行 rm -rf、git push --force、git reset --hard、DROP TABLE、terraform destroy、kubectl delete 等 | destructive |
WebFetch、WebSearch | untrusted + egress(URL 或查询内容可能会携带数据外传) |
mcp__server__tool | 代理的名称启发式规则 |
检查内容,在工具运行前:可能发送数据的调用中是否包含凭据(egress:credential);你的 rules——将 upstream 设为 claude-code 或 cursor,将 tool 设为工具名称(Bash、Read、WebFetch、mcp__github__create_pull_request),将 args 设为其输入,将 topic 设为其主题;以及致命三要素检查:会话一旦读取过不可信内容和敏感数据(秘密文件,或任何输出中的 PII),egress 调用就会被拒绝(taint.mode: strict)、询问是否批准(approve)或记录日志(warn)。工具运行后:输出中的 PII 会污染会话,输出中隐藏的指令会使 Agent 停止运行(decision: block 并附上原因),主题规则也会应用于输出。
rules:
- { id: ask-destructive, match: { upstream: claude-code, label: destructive }, action: approve }
- { id: no-prod, match: { tool: Bash, args: { command: "*--context prod*" } }, action: deny }
- 钩子不会替你回答“允许”:没有异议时,Agent 自己的权限提示仍会保留。
deny会变为permissionDecision: deny(Cursor 中为permission: deny),approve会变为ask。 defaults.action不会应用于 Agent 工具——否则,针对 MCP 工具设置的deny默认值会阻止所有 shell 命令。请为需要阻止的操作编写规则。- 每次钩子调用都是一个独立进程(约 0.1–0.2 秒)。会话污染状态按 Agent 会话保存在
~/.local/state/guardbee/hooks($XDG_STATE_HOME)下,并在一天后过期。审计日志会写入你配置的文件接收器,或~/.local/state/guardbee/hooks/audit.jsonl——绝不会写入 Agent 的终端;如果设置了audit.dashboard,也会写入仪表板。 - 配置顺序:
--config、GUARDBEE_PROXY_CONFIG、~/.guardbee/guardbee-proxy.yaml,最后是内置默认值。仅用于钩子的文件包含hooks: {}部分,且不包含 upstream。 - 发生内部错误时,工具仍会运行(故障开放),除非安装钩子时使用了
--fail-closed。 - 内置工具的输出不会被改写:遮蔽 Claude 读取的文件会导致它后续的
Edit调用失败,因为这些调用必须与文件内容完全匹配。
MCP 工具
代理不会注册自己的工具。它会转发目标服务的工具,并在每次调用时应用拦截器链。
拦截器
提示词注入检测器
@guardbee/guard-core 中的 27 条规则,应用于工具参数、工具结果、资源和提示词:
- 拦截(13 条精确度高、严重性为高/严重的规则):英文和土耳其语中的指令覆盖(“ignore all previous instructions”“önceki talimatları yok say”)、
DAN mode、“bypass your safety guardrails”、聊天模板标记(<|im_start|>、[INST])、<system>标签、隐藏在 Unicode 标签字符中的文本、CSS 隐藏元素或 HTML 注释中的指令、Markdown 图片外泄信标、要求泄露系统提示词的请求(英文和土耳其语)、“send the user's data to https://…”。 - 仅警告(14 条):也会出现在普通文本中的通用短语——“act as”、“you are now”、“developer mode”、“jailbreak”、“override policy”、“sen artık”——以及严重性中等的信号,例如零宽字符或双向控制字符,以及行首的
System:标签。 - **Base64:**可解码为可读文本的字符串会先解码,再根据拦截规则进行检查,因此指令无法藏在编码数据块中。
从 1.1 版开始,单独出现宽泛短语不再触发拦截:在 1.0 版中,工具结果中的“use jailbreak mode”或“act as a reverse proxy”会被拦截;现在则会记录该内容,并为结果附加警告。
PII 遮蔽器
| 数据类型 | 检查方式 | 示例输入 | 输出 |
|---|---|---|---|
| 国家身份证号(TC Kimlik No) | 官方校验和 | 10000000146 | [TC-KİMLİK] |
| IBAN(TR) | mod-97 | TR330006100519786457841326 | TR**[IBAN] |
| 卡号 | Luhn 算法 | 4111 1111 1111 1111 | ****-****-****-[KART] |
| 电子邮件 | — | [email protected] | ***@[EMAIL] |
| 电话号码(TR) | 手机号 / 固定电话 / 850 前缀;(0532) … 也适用;裸 5xx 号码仅在 numaram、tel、cep 等词之后识别 | 0532 123 45 67 | +90-***-***-**[TELEFON] |
| 税号(VKN) | 校验位,需带 VKN / Vergi No 标签 | VKN: 1234567890 | VKN: [VKN] |
| 服务商密钥 | secret-scanner 中的 22 种密钥和令牌格式(AWS、GitHub、Stripe、OpenAI、Anthropic、Slack,……) | ghp_…、AKIA… | [API-KEY] |
| 其他 API 密钥 | sk_/api_/token_… 前缀,字母和数字 | api_k3y9x8… | [API-KEY] |
| 私钥 | 完整的 BEGIN…END 块 | -----BEGIN … PRIVATE KEY----- | [PRIVATE-KEY] |
| 连接字符串 | URL 中的用户名:密码(不包括 localhost) | postgres://app:pw@db/prod | [CONNECTION-STRING] |
| JWT | — | eyJhbGc... | [JWT-TOKEN] |
| 护照 | 需带 Pasaport / Passport 标签 | Pasaport No: U12345678 | Pasaport No: [PASAPORT] |
| SGK / SSK / Bağ-Kur 编号 | 需带标签 | SGK sicil no: 1234567890 | SGK sicil no: [SGK-NO] |
| MERSİS 编号 | 需带标签;16 位数字 | MERSİS No: 0123 4567 8901 2345 | MERSİS No: [MERSİS] |
| 出生日期 | 需带 Doğum tarihi / DOB / Date of birth,或紧随其后的 doğumlu;可以是数字或月份名称 | Doğum tarihi: 12.05.1990、3 Mart 1979 doğumlu | Doğum tarihi: [DOĞUM-TARİHİ] |
| 邮政地址 | 需带 Adres: / Address:(含数字或街道词;不包括 IP / 电子邮件 / 网站 / 钱包地址),或不带标签但符合 Mah. … Cad./Sok. … No: 格式 | Adres: Moda Cad. No 5、Gazi Mah. Akasya Cad. No:5 D:3 | Adres: [ADRES] |
| KEP 地址 | @….kep.tr | [email protected] | ***@[KEP] |
人名 (需启用 ner:) | 本地土耳其语 NER 模型(@guardbee/ner) | Ayşe Yılmaz aradı | [KİŞİ] aradı |
| IP 地址 (可选启用) | 有效的 IPv4 八位组;包含 :: 或 8 组的 IPv6 | 10.0.3.17 | [IP] |
| MAC 地址 (可选启用) | 6 组十六进制对,使用一种分隔符 | aa:bb:cc:dd:ee:ff | [MAC] |
| 车辆牌照(TR)(可选启用) | 省份代码 01–81、字母/数字组合;不识别单位(16 GB 2400) | 34 ABC 123 | [PLAKA] |
IP、MAC 地址和车牌号常见于基础设施输出中,因此默认不会遮蔽;除非你将它们列出:interceptors.piiMasking.include: [ip_address, mac_address, plate_tr]。每个类别都会计入 piiHits 和仪表板的 KVKK 报告。 |
选择要遮蔽的内容。 piiMasking.preset 用于选择执行遮蔽的检测器,enable / disable 可按名称添加或移除检测器(名称会显示在 piiHits 中,例如 tc_kimlik、email、github_pat)。upstreams.<name> 会替换某个上游服务或 LLM 提供商的检测器选择范围。检测范围不会缩小:每个默认检测器仍会计入 piiHits,并标记会话;只有遮蔽会遵循所选范围。可选启用的检测器不属于任何预设;请将它们列在 enable(或 include)中。
| 预设 | 遮蔽内容 |
|---|---|
ALL(默认) | 除可选启用的检测器外的所有上述检测器,以及凭据 |
KVKK | 仅个人数据,不包括凭据 |
KVKK_SENSITIVE | KVKK 加上 KVKK 第 6 条规定的九类特殊类别(见下文) |
IDENTITY | 人名(需使用 ner:)、邮政地址、出生日期 |
IDENTIFIERS | TC kimlik、VKN、MERSİS、SGK、护照、IBAN、银行卡、电话、KEP、电子邮件 |
SECRETS | API 密钥、令牌、私钥、连接字符串、JWT |
interceptors:
piiMasking:
enabled: true
preset: KVKK
disable: [address]
upstreams:
ops: { preset: SECRETS, enable: [ip_address] } # an infrastructure server
未知的预设、检测器或上游服务名称会导致配置验证失败。
自定义模式。 piiMasking.blockPatterns 可遮蔽检测器无法识别的内容(例如案件编号、客户端代码):可以是一个正则表达式,也可以是 { pattern, category };类别会成为带编号的占位符(CLIENT_NO → <CLIENT_NO_1>,相同的值会使用相同编号),并作为 piiHits 的键。发生重叠时,内置检测优先;无论预设为何,都会应用阻止模式。piiMasking.allowPatterns 会在检测到的值的全部文本匹配时将其排除在遮蔽之外(例如公开的支持邮箱地址);每次发生这种情况,都会向审计日志写入一个带有 ruleId: pii:allow 的 warn 事件,因为这会降低保护级别。
interceptors:
piiMasking:
enabled: true
blockPatterns:
- { pattern: "CL-\\d{6}", category: CLIENT_NO }
- "DOSYA/\\d{4}" # category CUSTOM
allowPatterns: ["destek@acme\\.com\\.tr"]
每个模式都有时间预算(每次遮蔽调用 100 ms),因此灾难性正则表达式不会让代理陷入停滞;两种模式有意采用相反的超时处理方式:阻止模式超出预算时会拒绝结果(pii:pattern-timeout;LLM 网关会返回 502),允许模式超出预算时则会跳过该模式,并继续遮蔽该值(pii:allow-skipped)。最多可配置 25 个模式,每个模式最多 200 个字符;无效正则表达式,或与内置检测器同名的类别(EMAIL、IBAN、SECRET 等)都会导致配置验证失败。LLM 网关请求可以在 guardbee 请求体字段中添加自己的阻止模式({ "guardbee": { "blockPatterns": [...] } },该字段会在提供商收到请求体前移除);这些模式会追加到已配置的模式中。若请求包含 allowPatterns,则会以 400 拒绝:请求只能增加遮蔽范围。
校验和不通过的数字(例如订单号、追踪码)会保持原样。
人名。 正则表达式无法识别人名,因此 person_name 由模型提供:@guardbee/ner,这是一款可离线运行的土耳其语 BERT NER 模型(186 MB,在笔记本 CPU 上处理每段文本约需 8 ms)。只有在配置中包含顶层 ner: 段时才会启用。该段属于部署配置,而非策略:仪表板策略无法设置或移动它。
ner:
url: http://127.0.0.1:8765 # guardbee-ner serve (sidecar)
tokenEnv: GUARDBEE_NER_TOKEN # its bearer token, from the environment
timeoutMs: 5000 # default
# or in this process (install @guardbee/ner next to the proxy):
# ner:
# modelDir: /opt/guardbee/ner # default: guardbee-ner download's cache
运行 sidecar 前,先执行 npx @guardbee/ner download,再执行 npx @guardbee/ner serve;它会监听 loopback 地址。位于其他主机上的 sidecar 必须使用 https://,否则文本会以明文形式通过网络传输。模型会在加载前使用固定的 SHA-256 哈希值校验其文件。
遮蔽前,值中的每个字符串都会先发送给模型一次,然后在同一轮处理中与正则检测器一起遮蔽,因此 person_name 遵循 preset / enable / disable、mode: tokenize(<pii:person_name:…>)和允许模式等规则。工具结果、structuredContent、资源、提示词模板、采样回答和 LLM 网关请求(包括嵌入)中的姓名都会被遮蔽,并计入 llm.sensitive 路由。启用 llm.maskResponses 后,模型回答中的姓名也会被遮蔽。在流式处理中,文本不会在首字母大写的单词旁截断,因此姓名会完整地到达模型。如果模型无法处理回答,回答的其余部分会被拦截([GuardBee: withheld, name detection failed])。编码 Agent 钩子会将工具输出中的姓名计入会话的污染状态;如果模型在此处失败,输出会被计为个人数据。每次调用都会启动一个钩子进程,因此请为钩子配置 sidecar(ner.url),而不是 modelDir。关闭遮蔽时不会查找姓名。
如果模型识别出的片段完全位于特殊类别术语内(例如 Huntington 位于 Huntington geni 中),则在运行 KVKK_SENSITIVE 时会由该类别处理。
设置 ner: 后,如果模型无法回答(sidecar 停机、超时或发生错误),则不会发送任何内容:工具结果会被拦截,LLM 请求会被拒绝,并返回 502 guardbee_masking_failed、ruleId: pii:ner-failed。遮蔽绝不会退回到仅使用正则的方式,因为文本中可能包含只有模型才能识别的姓名。
模型会漏掉一些姓名,也会误判某些词。句首大写且同时是姓名的词(Barış、Umut)可能被正确识别,也可能被误判;全小写的聊天内容(elifle konuştum)则经常被漏掉。实测数据见 pii-bench。
KVKK 第 6 条特殊类别。 preset: KVKK_SENSITIVE(或按名称通过 enable 启用)还会遮蔽健康、宗教和信仰、族裔来源、政治观点、工会成员身份、犯罪记录、性生活、生物特征和遗传数据:health → [SAĞLIK]、religion_belief → [DİN-İNANÇ]、ethnic_origin → [ETNİK-KÖKEN]、political_opinion → [SİYASİ-GÖRÜŞ]、union_membership → [SENDİKA]、criminal_record → [SABIKA]、sex_life → [CİNSEL-HAYAT]、biometric → [BİYOMETRİK]、genetic → [GENETİK]。这些类别通过每类一份土耳其语和英语术语表以及上下文规则进行识别。只有当同一段文本与某个人有关时,术语才会被遮蔽:例如模型识别出的姓名、个人标识符,或指向某个人的词语(hastanın、çalışan、babam、第一人称动词)。Diyabet hastaları için rehber 会保持原样;Çalışan migren nedeniyle raporlu 则会变为 Çalışan [SAĞLIK] nedeniyle raporlu。遮蔽片段会扩展到术语周围的短语,因此 metal işçileri sendikası üyesi 会整体被遮蔽。
这些检测器需选择启用,因为术语表无法区分医学知识库和患者档案。检测策略偏保守:文本一旦与某个人有关,其中列出的所有术语都会被遮蔽。它们识别的是词语,而非含义,因此不在列表中的术语会被漏掉。请将它们与 ner: 搭配使用,因为没有姓名时,许多文本只有通过姓名才能判断与某个人有关。在 pii-bench 中,手工编写的挑战集在使用姓名模型时各类别的 F1 为 86–100,不使用时为 40–89,且在其中的干净文本行上没有误报。每段文本的额外耗时低于 0.15 ms。
配置
可通过环境变量进行配置:
| 变量 | 默认值 | 描述 |
|---|---|---|
PROXY_MODE | block | block 或 warn |
PROXY_LOG | console | console 或 file |
PROXY_LOG_PATH | ./proxy-audit.jsonl | 日志文件路径 |
PROXY_PII_MASK | true | 启用 PII 遮蔽 |
PROXY_INJECTION_CHECK | true | 启用注入检查 |
许可证
MIT — GuardBee
反馈
有问题、想法或遇到误报?请在 GitHub 讨论区中发起讨论,或提交问题。如果 GuardBee 对你有帮助,可以在代码仓库点个 ⭐,帮助其他人找到它。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。