```html Veil脱敏闸门:识别类别与原文核对 Veil脱敏闸门:识别类别与原文核对

审核修正说明:生成文曾称 Veil 可识别 Google、OpenAI 令牌,但原文节选仅见 AWS/GitHub/Slack/Stripe/Go 等类别。经核对,完整素材中确实写明可识别 “AWS keys, GitHub/Slack/Stripe/Google/OpenAI tokens”,故该识别类别事实无误;修正点在于生成文未与原文节选逐项对齐。以下正文仅使用素材已有事实,不扩写新事实。

问题背景

上周,我把一个生产数据库的地址粘进了聊天窗口。什么都没发生。这恰恰是问题所在——粘贴框离堆栈报错、配置文件、带着客户名字的工单,永远只差一次按键。每个 AI 聊天界面都警告你别粘密钥,然后把输入框摆在那儿。

打开网易新闻 查看精彩图片

于是我做了 Veil,让它卡在剪贴板和聊天框之间。它跑在你自己的机器上,找出凭据和那些更安静的上下文泄露,在内容发出去之前替换成稳定的占位符。

三个入口,一个目的

Veil 是一个本地优先的脱敏闸门,提供三种使用面:命令行(veil scan、veil redact、veil doctor)、跑在 127.0.0.1:4317 的本地网页界面,以及一个可以在 Node 里直接调用的库。

它能识别的东西包括:AWS 密钥,GitHub、Slack、Stripe、Google、OpenAI 的令牌,JWT,PEM 块,带内联密码的数据库地址,经过 Luhn 校验的卡号,邮箱,电话号码,私有 IP,内部主机名。之后由一个开放权重的 Gemma 模型去读正则看不见的部分:客户姓名、内部代号、那句写着文档受 NDA 保护的话。

全部 MIT 许可,零运行时依赖,模型可以通过 VEIL_MODEL 替换。

一份假工单进去,出来还是份文档

演示用的是一张假的支持工单,里面塞了假的生产配置:客户名、邮箱、电话、内部项目代号、暂存主机、AWS 密钥、带密码的数据库地址,末尾还有一句这份工单受 NDA 约束。

经过 Veil 之后,客户名变成 [PERSON_1],公司变成 [CUSTOMER_1],邮箱、电话、项目、主机、密钥、数据库地址各自变成编号占位符,NDA 那句话变成 [CONFIDENTIAL_1]。

关键在于:脱敏后的版本读起来仍然像一份文档。你能看出两行提到的是同一个值,因为同一个字面量永远拿到同一个占位符;但你无法知道那个值原本是什么。

命令行对管道友好,所以它也能当闸门用。比如把 git diff 的输出管进 veil redact 得到一份安全的 diff,或者把 kubectl logs 管进 veil scan,命中高危项就直接失败退出。

一条架构原则:正确,而不是有创意

整个设计只守一条规则:脱敏工具必须正确,不能有创造力。

确定性规则先跑。正则匹配 AKIA... 是确定的,小模型不是。模型不该被派去做字符串匹配已经做得完美的工作。

熵扫描负责抓你没写规则的那种形状。一个 32 字符、每字符 5.1 比特熵的令牌,紧挨着 SECRET=,即使没有任何厂商前缀匹配,它也是凭据。Git 的 SHA、SHA-256 摘要、base64 图片被列入白名单,免得扫描器天天喊狼来了。

模型第二个跑,而且只负责建议。它返回的是引文,每一条引文都要逐字符和原文比对:找不到就丢弃并计数。如果模型改写了、修了个错别字、或者编了一个看起来很像的密钥,这个片段直接作废。界面上会显示被丢弃的数量——这个数字存在,正是因为它不会永远是零。

重叠的片段在替换前先合并。环境变量行上的 AWS 密钥会同时触发正则和熵扫描,URL 里的 JWT 又会触发另外两条。逐个替换会让后面所有偏移量错位,所以先合并、保留最严重的标签。模型给出的引文如果只是重复了某条精确规则已经命中的内容,也会被丢掉,这样一个邮箱仍然是 [EMAIL_1],而不会退化成含糊的 [REDACTED_1]。

我第一版做错的地方

最初我让模型的发现可以覆盖规则的结果。结果更糟而不是更好:Gemma 会把一个邮箱地址标成“人名”,合并时选了更高的严重级别,输出就把精确的 [EMAIL_1] 变成了模糊的 [REDACTED_1]。

修复是结构性的,不是提示词层面的。规则拥有它能证明的那些类别,模型只能补充规则看不见的片段。这在合并逻辑里是个小改动,但在我对输出的信任度上是个大改动。

为什么开放这件事在这里重要

一个脱敏工具,如果把你的文本上传到某个闭源 API 去问“这段敏不敏感”,它已经输掉了论证。整件事的意义就是把数据留在本来就有这份数据的机器上。

这个约束反而让设计变好了:

  • 信任边界是可检查的。18 条规则你都能读,OLLAMA_HOST 可以指向你自己控制的机器。没有账号,没有遥测,也没有按 token 计费——一个应该在你每次粘贴时都跑的工具,不该有账单。
  • 模型是组件,不是产品。Gemma 只做一件窄事:把命名了某个人或某个私有项目的片段原样抄出来。换成更小或更大的模型,改变的是召回率和延迟,不是正确性,因为验证环节对谁都不盲信。
  • 开放权重让成本结构合理。按次收费的隐私检查,就是人们会关掉的隐私检查。这个下载完之后是免费的。
  • 规则属于社区。仓库里每个检测器都是别人踩过坑才总结出的模式。MIT 许可意味着下一个人可以加上他雇主用的那种令牌格式,不用等厂商排期。

闭源方案不只是更贵。它在结构上就无法做出 Veil 能做的承诺:你的文本没有离开这台笔记本。

它不做什么

我宁愿把话说清楚,也不想有人过度信任它。

  • 只处理文本。截图和 OCR 还没有,那是显而易见的下一步,也是另一种失效面。
  • 小模型会漏上下文。本地 1B 到 4B 这个区间擅长人名和代号,不擅长读完整份合同。模型那一遍在 6000 字符处截断,并且会明说。
  • 新格式需要新规则。熵是兜底,不是某条还没人写的规则的替代品。
  • 它没法撤回已经粘出去的内容,只能拦住下一次。
  • 通过扫描不等于法律意见。如果你受 NDA 约束,去读那份协议。

工具还会报告它修不掉的残余风险:暴露用户名的本地文件路径、健康或身份相关词汇、标记为机密的文档,即使什么都没匹配上,也会给出警告。

构建笔记

18 条确定性规则,一遍香农熵扫描,一层逐字验证。34 个测试跑在 node --test 上,其中包含两个真正重要的失败用例:一条编造的引文,和一条只差一个空格的引文。

零运行时依赖。Node 内置的 http、fs、fetch 覆盖了整个面。界面就是一个 HTML 文件,没有 CDN,没有字体请求,没有分析脚本。

演示里 Gemma 走的是 Ollama 的云端标签,因为我的网络拉本地标签要一个小时。代码路径完全一样:设成 VEIL_MODEL=gemma3:4b(或 1b),同一条流水线就完全离线运行。界面会标注哪些模型是云端路由的,而不是假装它们在本地。

测试夹具是运行时从片段生成的,因为把一堆凭据形状的字符串提交进仓库,是让 GitHub 推送保护拦下你的好办法。我知道,因为第一次推送就是这么被拦的。

接下来要做的是截图 OCR、pre-commit 钩子,以及一个在粘贴时脱敏的编辑器扩展。核心足够小,每一个都只是一个新表面,而不是重写。