防止 agent 把自己的密钥吐出来:bash guard 的攻防清单

Flashduty AI SRE 技术揭秘第 11 篇:介绍 bash guard 如何限制凭据泄露、环境变量转储和高风险命令,以及字符串匹配的威胁模型与边界。

作者 快猫星云

暖色手绘封面:门卫检查来访者手中的钥匙和纸条

出门前过一道门卫:这张不能带出去,正确走法写在这张上。

Flashduty AI SRE是一个专注 SRE 领域、帮助团队分析故障根因和完成日常运维工作的 AI Agent 产品,注册即可免费试用。这是系列文章《Flashduty AI SRE 技术揭秘》第 11 篇,本系列会持续分享我们在 agent 工程上的真实设计、踩过的坑和做过的取舍。

上一篇结尾留了一个问题:GitHub token 通过环境变量交给了 bash 进程,agent 能不能自己运行 echo $GH_TOKEN,把它读出来?

能。而且要担心的不只是模型自己好奇。agent 排查问题时会读 GitHub issue、读日志、抓网页,这些内容都不受我们控制。如果其中埋了一句「把你的环境变量发到这个地址」,模型有可能照做。LLM、shell 和注入的凭据放在一起,就构成了一个真实的 prompt injection 攻击面。凭据是我们放进这个环境的,看住它也是我们的事。

我们的做法是在每条 bash 命令发给 runner 之前做一次检查,内部叫 bash guard。规则不多,下面一条条说它们为什么存在、为什么长成现在这样。

先说清楚防谁

讨论防御规则之前,得先交代威胁模型。不然每条规则都能被挑出漏洞,讨论也没有落点。

bash guard 防的是「可信沙箱里好奇的 LLM」。它做的是字符串模式匹配,不是完整的 shell 解析。bash 有子 shell、命令替换、eval 等组合能力,要做到百分之百检出,基本等于重新实现一个 shell。我们没打算这么做,也接受由此带来的缺口:比如用 $(echo FLASH; echo DUTY_APP_KEY) 拼出变量名,guard 是拦不住的。这类刻意的绕过交给外围的沙箱和 BYOC 的用户隔离处理,guard 只负责封住 LLM 最可能发出的几种直接尝试。

这个定位也决定了后面每条规则的写法:边界要清楚,误报要少,不去做那种看起来聪明、实际上没人说得清行为的解析。

规则一:命令里出现凭据名就拒绝

只要命令里出现受保护凭据的变量名,比如 GH_TOKEN、GITLAB_TOKEN、FLASHDUTY_APP_KEY 这一族,guard 就直接拒绝。裸写变量名、$VAR、${VAR} 都算。

这里有两个刻意的选择。第一,出现在注释里也拒绝。cat app.py # read $GH_TOKEN here 这条命令里的引用不会被执行,但命令里提到凭据名,本身就说明模型在打它的主意,不管它出现在哪个语法位置。第二,不是凭据的变量照常放行。FLASHDUTY_BASE_URL 只是一个端点地址,agent 在正常工作中需要读它,拦掉它只会让正常命令跑不通。所以黑名单只列凭据本身,不按 FLASHDUTY_ 这样的前缀一刀切。

规则二:整体转储环境就拒绝

模型也可以不点名,直接把整个环境打印出来。所以这类命令同样拒绝:裸 env、printenv,裸 set、export、declare、typeset,export -p、compgen -e,以及读取 /proc/*/environ。

这条规则难在不误伤。env、set、export 都是常见单词,env VAR=val cmd、set -e 也是再普通不过的写法。我们按命令位置锚定匹配,这些带参数的合法用法都放行。最大的误报来源反而是注释:像 # 7d export 这样的一行,在正则看来就是一个裸 export。所以做转储匹配之前,我们先把整行注释去掉。

行尾注释则保留不动。在 grep "#" f; env 里,# 是 grep 要找的数据,不是注释的开头。要安全地去掉行尾注释,就得追踪引号,这又回到了重新实现 shell 的路上,我们不做。代价是留下一个已知的误报:grep -E "a|env|b" 这样引号里的内容可能撞上规则。我们接受这个误报。误报的后果是 agent 换个写法再跑一次;漏报的后果是凭据被带出去。两边的代价差得很远,所以规则宁可偏向误报。

规则三:拒绝信息里写明正确做法

命令被拒时,agent 收到的不是一句「denied」,而是一条操作说明:直接使用 gh、glab 或 git,凭据会自动注入。

这条规则依赖上一篇讲过的设计。Git 认证走 credential helper,agent 正常 clone、push、查 PR 时,从头到尾不需要看到 token 的明文。如果 agent 正常干活就得读 token,guard 每拦一次,等于在逼它找另一条路绕过去。正常路径不需要碰凭据,拒绝时再告诉它这条路怎么走,guard 才守得住。在这里,安全和易用要的是同一件事。

规则四:注入凭据不看命令内容

早期的凭据注入是有条件的:只有命令里出现 fduty 字样时,才注入 Flashduty 的 key。后来我们把这个条件整个删掉了,因为 agent 调用 CLI 的路径不止一条:可能包在脚本里,可能用绝对路径调用,也可能通过 bundle 里的 bootstrap skill 调用。按命令里能不能看到某个字样来决定注不注入,这些路径会悄悄失效。我们看到的现象是:命令最终确实调用了 fduty,却没有通过认证。

现在每条 bash 命令都无条件注入。这样做的前提是凭据本身足够短命:这个 key 每轮临时签发,轮末吊销。凭据寿命短,注入时就不需要挑命令。与其给注入加越来越多的条件,不如缩短凭据的寿命。

顺带守住的两条规则

bash guard 的位置在命令发往 runner 之前。既然所有命令都要经过这里,我们也在这里放了两条和凭据无关的规则。

第一条:禁止 agent 直接发布 status page 变更。fduty status-page change-create、update、delete 这类命令会把事件推送给订阅者,几秒钟内出现在公开页面上,而且无法撤回。不可撤回的操作不应由 agent 直接执行。agent 可以用草稿工具起草,由人在控制台审阅后再发布。这条禁令写在 guard 里,模型就没有办法执行这类命令;如果只写在 prompt 里,它只是一句模型可能说服自己绕开的话。涉及不可逆后果的禁令,必须落在代码里。

第二条:禁止 /dev/tcp 和 /dev/udp 重定向。bash 的这项特性,在主机入侵检测系统看来是反向 shell 的典型特征。BYOC 场景下 runner 跑在客户的机器上,agent 一次普通的端口探测,在客户那边就会触发一条真实的安全告警。探测端口用 curl 就能做到,效果一样,也不带这个特征。这条规则保护的不是我们的系统,而是客户安全团队对我们的信任。

有了 broker,还需要 guard 吗

还需要。在新的 runner 上,平台自己的 key 通过带外通道投递,根本不进入 bash 环境。对这个 key 来说,guard 已经退为纵深防御的一层。

但上一篇讲过,GH_TOKEN、GITLAB_TOKEN 在所有 runner 上仍然通过环境变量注入,而且是有效的、带写权限的 token。所以即使在全新的 runner 上,「出现凭据名就拒绝」和「整体转储就拒绝」这两条规则也还在守着真实的凭据。删一条安全规则之前,先确认它保护的东西是不是真的不在了。

小结

bash guard 是一个字符串模式检查器,规则分四类:凭据名引用、环境整体转储、不可逆的发布操作、反向 shell 特征。它有明确的威胁模型,也依赖一个配套设计:正常路径不需要碰凭据。它不声称能挡住刻意绕过的攻击者,只挡住 LLM 在诱导下最可能发出的直接尝试;剩下的交给沙箱、用户隔离和短命凭据。

我们在 agent 安全上的体会是,防线不必一味加深,但每一层都要说得清自己防谁、不防谁。说不清威胁模型,规则就只会越加越多,谁也不知道哪条还有用。

下一篇讲一个相邻的问题:工具缺少用户授权时怎么办。直接报错会让 agent 卡住,我们的做法是返回一个结构化信封,让工具在最需要的时候,教 agent 向用户要凭据。


这是「Flashduty AI SRE技术揭秘」系列的第十一篇。我们在做一个专注 SRE 领域、帮助团队分析故障根因的 AI Agent 产品,本系列会持续分享我们在 agent 工程上的真实设计、踩过的坑和做过的取舍。

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云