把密钥拦在模型之前:一个本地代理的做法,和三种拦截层次的取舍
先说结论
- 密钥泄漏的主入口不是「你手打进对话框」,而是 agent 自己去读的东西:源码文件、shell 输出、截图、PDF。一个本地代理项目的作者把这句话写在动机里——「Coding agents read source files, shell output, screenshots, and documents. Secrets can arrive through any of them.」
- 它最值得抄的不是规则集,而是两个设计取舍:默认脱敏而不是拦断(把命中的密钥替换成
[REDACTED:rule-id],请求照常发出去,不打断 agent),以及接入成本只有改一个 base URL。 - 但任何单点都防不全。本机代理看不到你手动粘进网页版聊天框的内容,自建网关拦不住 agent 从本地文件里读出来的东西,而供应商侧的检测是最后的兜底——到那一步数据已经出境了。真正有效的做法是按层次组合,并接受每层都有盲区。
问题到底出在哪一层
代理式编程工具的工作方式是:自己决定读哪些文件、跑哪些命令、看哪些截图。这意味着你要防的不是用户主动泄密,而是工具”顺手”把敏感内容带进上下文。
最典型的三个场景:
.env被当成普通文本读进来:agent 想搞清楚配置怎么加载,于是把整个文件读进上下文。- 命令输出里带凭证:
env、printenv、CI 日志、git remote -v、连接串报错信息。 - 截图和 PDF 里有密钥:控制台截图、部署文档、客户发来的配置说明——这类最难防,因为它是图片。
这个工具做了什么
被引的项目叫 preflight(github.com/ghuntley/preflight,出现在 Hacker News 上),自我描述是「A local proxy that scans LLM requests and attachments for secrets before they reach the model」。
链路比”本地端口”复杂一点:它不是直连模型,而是串在另一个代理后面——preflight 监听 127.0.0.1:8081,转发给上游 127.0.0.1:8080(作者的另一个项目 underclass),启动前 underclass 必须已经在跑。文档里的启动方式是一条 Nix 命令,因为它要带一整套文档处理工具链(PDF 解码、OCR、元数据、条码识别)。
上之前先知道三件事
一、命中之后有三种行为,不是只有”脱敏”。 这一点最容易被误解,我把它单列出来:
| mode | 行为 | 什么场景用 |
|---|---|---|
redact(默认) |
替换文本命中项、清洗支持的附件;当某个命中没有安全替换方式时返回 HTTP 409 | 日常使用:不打断 agent,但有兜底 |
no-go |
任何非白名单命中直接 409,请求根本不到上游 | 强合规场景:宁可停也不放 |
advisory |
只报告、原样转发——密钥仍然会到模型 | 调规则、观察误报,不能当防护 |
二、它不做”通用随机性检测”。 README 里那句说得很直白:prompts 和源码本身就是「entropy soup」(熵的乱炖),所以默认规则只认有明确形态的凭证。这解释了它的误报策略,也说明了它的边界——你公司自研格式的 key 大概率识别不出来。
三、它是全程检查完才转发。 「Nothing forwarded halfway through inspection — the whole request is checked before it goes to [upstream]」——不是边发边扫,这避免了”半截请求已经出去了才发现有问题”。
几条实现事实:项目自有可执行文件全部用 Rust;规则来自 Gitleaks 的规则数据库快照(vendor 目录里带上游许可证与 commit/校验和清单),实际启用哪些规则由 profile 文件显式挑选——所以上游新增规则不会静默扩大拦截范围;部署上主推 Nix 包 + 系统服务,不是”每台机器手动装一个”。
三种拦截层次的取舍
这是我认为比”推荐一个工具”更有用的部分。三层各自能防什么、防不了什么:
| 层次 | 放在哪 | 能防住 | 防不住 | 代价 |
|---|---|---|---|---|
| 客户端/本机代理 | 你的机器上,拦截出站请求 | agent 读文件、跑命令、读截图带出来的密钥;可在出境前拦下 | 你自己手动粘进网页聊天的内容;没走这个代理的客户端;代理被绕过 | 每台机器都要装;规则误报会打断工作流 |
| 自建网关 | 团队统一出口 | 全团队策略统一、可审计、可强制;能记录”谁在什么时候发了什么” | agent 从本地文件读出的内容——它只是一条网络通道,看不懂语义;本机直连外部 API 的流量 | 要维护;可能影响流式响应与 tool-call 结构 |
| 供应商侧 DLP | 模型服务商那边 | 最后的兜底;能覆盖所有路径 | 拦不住已经发生的事——数据已经到了对方系统;能力与策略不透明 | 你无法控制,也无法验证 |
一句话:本机代理是性价比最高的一层(能拦在出境前、误报可控),网关是治理层(管人和审计),供应商侧只能当保险,不能当前线。
一份可以照着做的自检清单
不管你上不上代理,先把「什么不该进 prompt」列清楚。按危害排序:
- 长期凭证:云厂商 AK/SK、GitHub PAT、API Key、SSH 私钥、数据库账号密码。
- 会话类凭证:Cookie、Bearer Token、OAuth refresh token(短命但可直接冒用)。
- 连接串:
postgres://user:pass@host/db、Redis/消息队列地址(等于半个内网地图)。 - 内网信息:内网域名、IP 段、堡垒机地址、内部服务名。
- 客户与个人数据:真实姓名+联系方式、订单号、身份证/手机号、医疗与财务记录。
- 未公开的业务信息:价格表、合同条款、未发布的功能设计。
最重要的一条:命中就按”已泄漏”处理。 无论用哪一层拦截,先轮换凭证、再排查泄漏路径、最后才是加拦截层。拦截层的意义是”减少下一次”,不是”撤销这一次”——已经进过上下文或日志的密钥,只能靠轮换作废。
再给你三个马上能做的动作:
- 给 agent 划定可读边界:把
.env、*.pem、credentials*放进忽略规则(各家工具的 deny 规则,或在项目说明书里写清楚)。 - 别让 shell 输出裸奔:
printenv、env这类命令的输出在 agent 上下文里等于明文。注意这条只是减少重复泄漏,已经执行过的那一次已经在上下文和工具调用记录里了:
# 需要看环境变量时,先把明显的凭证滤掉再交给 agent
env | grep -viE 'key|token|secret|pass|credential|session|cookie'
# 顺手确认这类文件没被版本控制跟踪(被跟踪了就等于已经出过一次门)
git check-ignore -v .env .env.local '*.pem' 2>/dev/null || echo "注意:这些文件没有被忽略"
- 截图前先打码:你要贴控制台截图给 agent 看时,遮住凭证区域——这是唯一一个”成本为零”的防护。
命中之后长什么样,README 的描述是”替换而不是报错“,形如 [REDACTED:rule-id]。下面这段是我按这个描述自拟的示意(不是实际运行输出):
替换前: export OPENAI_API_KEY=sk-live-9f3a...c21
替换后: export OPENAI_API_KEY=[REDACTED:openai-api-key]
这个取舍是整个设计里最实用的一点:请求照常发出去,agent 的工作流不被打断——如果一命中就报错,团队大概用两天就会把它关掉。
这次没核实的
- 我没有在本机跑 preflight,性能、误报率、OCR 准确率都无从判断;文中的架构、三种模式与实现事实都来自它的 README 与仓库文件。
- 规则来自 Gitleaks 规则库:能覆盖常见凭证形态,但对自定义格式的凭证(公司内部 token、自研系统的 key)大概率漏——README 自己也说了不做通用随机性检测。
- 上面那段”替换前/替换后”的示例是我按 README 描述自拟的,不是实际运行输出;README 里给的日志示例长这样:
[REDACTED:github-pat]。 - “脱敏后请求照常发出”的副作用:模型会看到
[REDACTED:rule-id],这在某些任务里可能让 agent 判断失误——这一点仓库里没展开。 - 本文只覆盖了代理这一种形态,其他方案(IDE 插件侧拦截、SDK 层包装)没有评估。
参考来源
- ghuntley/preflight——自我描述、三种 mode 的对照表、
bind/upstream配置、Gitleaks 规则快照与工具链要求 - gitleaks——本文所述规则库的上游项目(preflight 用的是它的规则数据库快照)
- OpenAI 兼容 API 参考——「保留 OpenAI 兼容端点与 tool-call 结构」这句话的对照面,换 base URL 即可接入的前提
- 该项目在 Hacker News 上的讨论:本次没有拿到可点开的讨论链接,所以不给链接、也不给日期(避免用首页充数)
评论区
登录后可评论。