关于 security-proxy
security-proxy 是位于 MCP 服务前端的安全代理,可集中执行注入检测、PII 遮盖、策略控制和审计记录。它也支持将多个服务置于统一网关之后,并跟踪跨服务的危险数据流。
主要功能
- 01检测提示词与工具结果注入
- 02遮盖响应中的 PII 和密钥
- 03按策略阻止或警告请求
- 04跟踪并阻止跨服务危险数据流
- 05记录请求审计日志
适用场景
- 01为现有 MCP 服务增加安全拦截
- 02通过统一网关管理多个服务
- 03阻止敏感数据流向外部工具
MCP 相关
它作为 MCP 代理连接上游 MCP 服务,并将其工具和提示词提供给客户端。代理可检查请求与结果、执行策略并记录审计事件。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
@guardbee/mcp-security-proxy
🇬🇧 English | 🇹🇷 Türkçe | 🇨🇳 中文 | 🇯🇵 日本語 | 🇰🇷 한국어 | 🇧🇷 Português | 🇪🇸 Español | 🇩🇪 Deutsch
一个透明的安全层,部署在任意 MCP 服务前端。它会阻止提示词注入攻击、屏蔽响应中的 PII,并将每个请求写入不可篡改的审计日志。
遥测默认关闭。设置
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的结果。之后描述或架构发生变化即构成 rug pull(MCP03:2025)。阻止模式会继续提供已锁定的定义,并拒绝调用发生偏移的工具 - PII 屏蔽 — 响应中的 TC Kimlik No、VKN、IBAN、银行卡号和电话号码(经校验和验证)、护照、SGK、MERSİS、出生日期、邮政地址和 KEP 地址、电子邮件、可选检测的 IP/MAC/车牌、22 种服务商密钥格式、私钥和连接字符串均会被屏蔽
- 阻止 / 警告模式 — 每个拦截器都可以独立运行于阻止或警告模式
- 审计日志 — 可配置写入控制台或文件的日志
- 网关模式 — 通过一个代理连接多个 MCP 服务,使用一份 YAML 策略(允许 / 拒绝 / 屏蔽 / 警告)
- 有害数据流阻止 — 跨服务跟踪致命三要素,并阻止会使其形成闭环的出站调用
- 大模型 API 网关 — 兼容 OpenAI 和 Anthropic 的端点:使用网关密钥代替服务商密钥,在提示词上执行 PII 屏蔽和注入检查,设置令牌和支出预算,并在审计日志中记录用量和成本
- 编码 Agent 钩子 —
guardbee-hook将相同策略应用于 Claude Code 和 Cursor 自带的 shell、文件和网络工具 - 无需更改代码 — 可部署在任何现有 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还会清空文本和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>;approve在此会拒绝请求,因为 API 调用没有可请求批准的对象。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 会改用操作系统沙箱运行它:
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
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运行)。
| 运行时 | 操作系统 | 文件系统 | 隐藏凭据 | 无网络 | 限制 |
|---|---|---|---|---|---|
docker / podman | Linux、macOS、Windows | 仅限已挂载的路径;根文件系统只读,--cap-drop ALL,no-new-privileges | 在挂载点内(tmpfs / /dev/null) | --network none | cpus、memory、pids |
bwrap (bubblewrap) | Linux | 整个磁盘只读,--unshare-all | 使用 tmpfs / /dev/null 覆盖这些文件 | 不共享网络命名空间 | — |
sandbox-exec (Seatbelt) | macOS | 整个磁盘可读,禁止写入 | deny file-read*(对任意深度的 .env 生效) | deny network* | — |
容器使用 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),并允许写入作为参数传入的目录。- 模型的限制。 Apple 已弃用
sandbox-exec,但它仍是 macOS 的沙箱机制;它不限制 CPU 或内存。bwrap需要非特权用户命名空间(某些发行版会通过 AppArmor 限制它们)。在非容器运行时中,未列出的文件不会被隐藏,仍可读取:只有凭据列表、.env文件和fs.deny会被隐藏,其他文件仍可读取(如需允许列表,请使用容器)。stdio 服务的network: open只能全开或全关;主机允许列表适用于 HTTP 上游(upstreams.<name>.network)。Windows 没有内置运行时:请使用docker或podman。
安全报告
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 会生成 CycloneDX 1.6 AIBOM,描述网关在 Agent 前面配置的内容:每个 MCP 服务都作为一个组件,包含其包 URL(pkg:npm、pkg:pypi、pkg:oci)、固定时的版本、来自来源缓存的构建来源状态和源代码仓库,以及沙箱设置;每个服务都作为一项服务,其中嵌套了其工具(名称、描述、固定到该定义的 SHA-256、声明的提示);远程服务包含其端点;LLM 网关的路由则作为带有提供商信息的 machine-learning-model 组件。它会读取配置、固定文件和来源缓存,不会发出任何请求;报告中绝不会包含环境值、标头和密钥。输出通过官方 CycloneDX 1.6 架构验证(已测试)。
可观测性(OpenTelemetry)
每次工具调用都会创建一个名为 tools/call <tool> 的跨度(类型为 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。对上游的调用是一个子跨度(类型为 CLIENT,server.address = 上游)。每个调用的审计事件 — 拦截、恶意流、审批、警告、服务端的采样请求 — 都是一个跨度事件(guardbee.blocked 等,并带有 guardbee.rule_id 和 guardbee.reason);被拦截的调用会带有 guardbee.blocked: true。参数和结果绝不会写入跨度。
该代理仅依赖 @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。 -
没有密钥,就没有网络。 若要监听回环地址以外的地址,必须配置
listen.apiKeys或listen.oidc,否则会出现配置错误。密钥采用恒定时间比较;密钥错误或缺失时会返回401。 -
网络出站(
upstreams.<name>.network): 代理向 HTTP 上游发起的每个连接(包括每次重定向跳转)都会在连接时进行检查:解析主机名、检查每个地址,并让套接字连接到已检查的地址,因此检查与连接之间发生变化的 DNS 响应(重绑定)无法访问内部主机。任何编码格式(八进制/十六进制 IPv4、IPv4 映射或 NAT64 IPv6)下的链路本地地址(例如169.254.169.254等云元数据地址)、0.0.0.0和保留地址范围都会被拒绝。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 服务会自行打开套接字;要限制这类连接,需要使用操作系统沙箱,本节不涉及。 -
目录组(
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 和大模型端点使用承载令牌进行身份验证,而使用 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),采用八位字节计数(默认)或换行分帧;默认使用 facility 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}。会话数超过maxSessions时,返回503。 -
仪表板:
audit.dashboard会批量向 GuardBee 仪表板发送每条审计事件,并使用从apiKeyEnv获取的工作区 API 密钥。密钥不会存放在文件中。如果无法访问仪表板,事件会在内存中等待(最多 10,000 条),之后再发送。密钥被拒绝时,会在 stderr 上报告一次。工具调用不会等待仪表板。 -
init现在也会将 Streamable HTTP 服务(url+headers)移至代理后面。旧版 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
大模型 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
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 });
每个请求都会依次经过以下步骤:
- 密钥。
Authorization: Bearer(OpenAI SDK)或x-api-key(Anthropic SDK)。来自llm.keys的网关密钥会标识用户和组;此外,也接受listen.apiKeys和listen.oidc令牌。如果设置了llm.keys,而listen未配置密钥,则始终拒绝未知密钥,即使请求来自环回地址也不例外。 - 路由和访问权限。 模型名称决定提供商;如果没有路由可服务该模型,则返回
404。llm.access决定调用者可以请求哪些模型:每个与用户(glob)或组匹配的条目都会添加其模型 glob;如果没有匹配的条目,则应用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 的本地模型),则会拒绝该请求,而不会将其发送到原本要避开的提供商。预算按路由后的模型计算。 - 预算。 按用户(或密钥)或整个网关统计固定时间窗口内的请求数、令牌数和美元金额。只要所有匹配的预算都有剩余额度,请求就会获准;令牌数和费用只有在收到回答后才可确定,因此触及限额的那次调用会正常完成,下一次调用则会收到带有
Retry-After的429。 - PII(
interceptors.piiMasking):消息文本、工具调用参数和工具结果会被遮蔽;或者在使用mode: tokenize时,将其替换为令牌,代理会在回答中将令牌还原为真实值——提供商看不到这些值,用户则可以看到。Anthropicdocument块中以文本或内容为来源的文本也会被遮蔽;图像数据、ID 和工具名称不作处理;代理无法读取的内容会被拒绝(参见失败行为)。 - 审计。 记录一条
tool_call事件(tool: llm:<model>、upstream: <provider>、labels: [llm]、user、piiHits),以及一条包含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 会列出调用者可使用的字面(非 glob)路由。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: 这样的标签和值位于下一个事件中,该标签仍然适用。仍在暂存的内容会在该选项的结束块或该区块的 stop 事件发出前发送。对于 Responses API,output_text、函数调用参数和推理摘要增量会以相同方式暂存并重写;每个 .done 事件、项目、部分以及最终的 response.completed 都包含相同的重写文本,因此 SDK 组装出的答案与增量一致。提供方错误会原样传递,因此 SDK 重试和错误类型仍能正常工作。
嵌入
POST /embeddings 会在嵌入前屏蔽每段输入文本;返回的向量保持不变。为已屏蔽的文本建立索引前,请注意:
- 此处的屏蔽具有确定性。 内置值会使用固定占位符(
[TC-KİMLİK]、***@[EMAIL]),不使用mode: tokenize(token 按请求生成,因此同一文档每次嵌入结果都会不同),组织屏蔽模式则按文本编号,而非按请求编号。同一文本始终生成相同的向量,无论它属于哪个批次。 - 仅在已屏蔽值上存在差异的文档,嵌入结果相同。 仅在人员姓名或 ID 号码上有差异的两条记录会合并为一个向量,因此相似度搜索无法区分这些人员。请按自己的 ID 字段(元数据)筛选,而不是依赖向量相似度。
- 索引和查询都使用相同路径。 根据已屏蔽文本生成的向量,只能与根据其他已屏蔽文本生成的向量进行比较:请将待索引的文档和待搜索的查询都通过网关发送,并使用相同的
piiMasking设置。 - 长度限制针对每个元素。
input中的每个元素都会按llm.maxTextChars检查;如果某个元素超出限制,则整个批次会被拒绝,并返回413。无法屏蔽 token ID 输入([[1, 2, 3]]),因此会以415拒绝——请发送文本。 - 嵌入不应用以下检查:提示词注入扫描(关于注入的文档仍必须能够建立索引)和
llm.sensitive路由(其他模型生成的向量无法与您的索引进行比较)。凭证、主题、预算和访问检查照常应用。
失败行为
网关采用故障关闭策略:无法检查或屏蔽请求时,不会调用提供方。拒绝响应会在错误正文(OpenAI 和 Anthropic 错误格式中的 error.code)和 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、未知部分或块类型,或非文本、非列表内容。消息会指出检测到的内容。 | pii:unmaskable |
413 | guardbee_text_too_large | 单段文本(消息、部分、工具结果)超过 llm.maxTextChars(默认 2,000,000 个字符)。限制适用于单段文本,而非整个请求:请将文本拆分为多条消息或多个部分。 | pii:text-too-large |
不会被拒绝的内容:图像(image_url、Anthropic image)会原样转发,不会经过检查;在助手轮次中,提供方自身生成的块(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 事件。
编码 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 --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: {}部分,且没有上游。 - 发生内部错误时,工具仍会运行(故障时放行),除非安装钩子时使用了
--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
运行边车服务:先执行 npx @guardbee/ner download,再执行 npx @guardbee/ner serve;它会监听本地回环地址。位于其他主机上的边车服务必须使用 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 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。