安全深入——权限与隔离
Hermes Agent 官方教程第29篇:权限与隔离
安全深入——权限与隔离
聊到 Hermes Agent 的安全,很多小伙伴第一反应是“不就是装个 Docker 吗?”——其实远不止。官方文档里写得很明白,这是一套 纵深防御(defense-in-depth) 模型,整整八层防线,从“谁能跟你说话”一路管到“容器里能不能写文件”。今天咱们就一层层拆开看,重点聊聊权限审批和隔离这两块硬骨头。
八层防线,层层设卡
简单过一遍这八层,你就知道 Hermes 有多“惜命”了:
- 用户授权——谁能跟 Agent 对话(白名单、DM 配对)
- 危险命令审批——破坏性操作必须有人类点头
- 文件写入安全——
write_file/patch的黑名单和可选沙箱 - 容器隔离——Docker/Modal(以及 Singularity 插件)加固配置
- MCP 凭据过滤——子进程环境变量隔离
- 上下文文件扫描——检测项目文件里的提示注入
- 跨会话隔离——会话之间互不可见,cron 存储路径防目录穿越
- 输入净化——终端工具的工作目录参数做白名单校验,防 shell 注入
每一层单独看可能都有漏洞,但叠在一起,攻击者想穿透就得“过五关斩六将”。
危险命令审批:三种模式随你挑
核心配置在 ~/.hermes/config.yaml 里,approvals.mode 有三个档位:
approvals:
mode: smart # smart | manual | off
timeout: 300 # 等用户回复的秒数
cron_mode: deny # cron 任务撞上危险命令咋办
single_query_mode: deny # 单次查询(-q)会话撞上咋办
unattended_mode: deny # webhook/API 等无人值守会话撞上咋办
mcp_reload_confirm: true # /reload-mcp 前先问一声
destructive_slash_confirm: true # /clear、/new 等破坏性命令先确认
- smart(默认):让一个辅助 LLM 当“风险评估员”。
python -c "print('hello')"这种低风险命令直接放行;真危险的自动拒绝;拿不准的才弹窗问你。 - manual:所有危险命令一律人工审批,一个都不放过。
- off:全部跳过,等于裸奔。文档里特意加了警告——只建议在 CI/CD、容器这类可信环境里用。此时
trust: untrusted的 MCP 服务器上的写入调用也不再弹信任提示;但 MCP 服务器声明的消费上限仍会询问。
YOLO 模式:作死开关,但有底线
YOLO 模式会绕过所有危险命令审批,激活方式有三种:
- 启动时加
hermes --yolo或hermes chat --yolo - 会话里敲
/yolo切换开关 - 设置环境变量
HERMES_YOLO_MODE=1
注意 /yolo 是开关,敲一次开,再敲一次关。开启后界面会有两个明显的视觉提醒:会话开头的红色横幅 ⚠ YOLO mode — all approval prompts bypassed,以及状态栏里的 ⚠ YOLO 标记,生怕你忘了自己在作死。
YOLO 在 CLI 和网关会话里都能用,但 /yolo 只作用于你敲它的那个会话。而且它会跟着会话一起存盘,重启也不丢:消息平台的聊天能扛过网关重启,TUI 和桌面 App 重新打开或恢复会话时也照样保留。不过在消息平台上,/new 和 /resume 开的新对话会把审批重新打开——想继续放飞得再敲一次。
但有个底线:硬性黑名单(hardline blocklist)永远拦着,YOLO 也绕不过去。也就是说,再怎么放飞,真正要命的命令还是会被摁住。
YOLO 和 approvals.mode: off 还会跳过 trust: untrusted MCP 服务器的信任提示:不可信服务器上的写入型工具会直接运行,作用范围按各来源的常规规则。/yolo 只管你敲它的那个聊天(别的聊天照样会问);--yolo 和 HERMES_YOLO_MODE 覆盖它们所作用的那个 Hermes 进程的会话;approvals.mode: off 覆盖该 profile 的所有聊天和 cron 任务。它们不会跳过:
- MCP 服务器的消费上限:会让聊天超出已声明
spend_limit的调用仍会询问,没人能回答时(cron 任务、hermes chat -q)直接拒绝。这是你设的预算,不是安全提示。 - MCP 服务器发来的提问(elicitation):那是向你要输入,不是要权限。
- 凭据保险库的支付卡填写提示:那是把卡交给页面;以及 MCP 事件订阅,那是安装常驻触发器。
审批超时与无头模式
几个值得注意的点:
timeout默认 300 秒:审批请求发出后,用户 5 分钟没回应就超时。超时不算拒绝,也不会被“记仇”——想继续干,重新发条消息让它再跑一次就行,会弹一张新的审批卡,而“批准一次”只对那一次调用有效。cron_mode、single_query_mode和unattended_mode:cron 任务、-q单次查询、以及 webhook / msgraph_webhook / api_server 这类无人值守平台上的会话,都是“没人能回答审批”的场景,默认deny——撞上危险命令直接拦下(不用干等整个超时),Agent 得另想办法;也可以设成approve全自动放行。有个例外:api_server 会话如果客户端能回答审批卡(/v1/runs和流式 chat completions,通过POST /v1/runs/{id}/approval),那它照样会收到审批请求。destructive_slash_confirm:/clear、/new、/reset、/undo这些毁会话的命令,现在会弹三选项对话框(批准一次 / 总是批准 / 取消)。在 Telegram、Discord、Slack 上走原生按钮,其他地方文字回退。TUI 里也认这个设置,HERMES_TUI_NO_CONFIRM=1可以强制跳过。
内容级命令检查
危险命令检测器除了盯“破坏性动词”,现在还多认两种内容层面的形态:
- 带密钥的
curl/wget请求体:比如密钥命名的变量(-d "k=$OPENAI_API_KEY")、凭据文件(-F file=@.env、-T ~/.ssh/id_rsa、--post-file=/etc/passwd),或者把凭据文件管道喂给上传用的curl/wget。注意,普通的Authorization请求头属于正常 API 用法,不会被标记。 - 不可见或双向 Unicode 控制字符(零宽空格、从右到左覆盖、隔离符等):它们会让你“批准的命令”和“实际跑的命令”不是同一条。Emoji 的连接符序列不算。
这两种都跟其他危险模式一样,走正常的审批流程。另外提一句:早先版本在这里捆绑了外部 tirith 扫描器,现在已经移除——升级会顺手删掉 security.tirith_* 那套设置,也不会用别的东西顶上。
execute_code 脚本
execute_code 跑起来的脚本可以直接调 subprocess、os、shutil,这些调用根本不经过上面那些模式。所以消息平台、API 等网关会话会在脚本开跑前整脚本问一次;被拒或没人应答,脚本就不会执行。cron_mode、single_query_mode、unattended_mode 会像对待单条命令那样立刻决定脚本命运;YOLO 和 mode: off 则跳过询问。
本地会话(经典 CLI、TUI、桌面 App、ACP 编辑器)走的是交互式 CLI 的老规矩:在 manual(以及 smart)下,用户自己敲的那一轮不做整脚本审批——脚本里的 Python 级文件、进程操作直接跑,只有脚本里的 terminal() 调用会逐个弹审批,走会话自己的提示。这个豁免需要两个条件同时满足:这一轮是用户提交的,且脚本启动时有能回答审批的用户在线。以下情况在本地会话里仍然要整脚本审批:
- 没人敲过的轮次:插件
inject_message、委派完成、后台进程和 watch 模式通知、/loop心跳、托管房间任务 delegate_task子任务和/bg//btw旁支任务- 用户那一轮执行时没有能审批的用户在线
别直接改控制文件
有一点要特别提醒:不要让 Agent 直接去 patch ~/.hermes/cron/jobs.json 这类控制文件。正确姿势是用 cronjob_manage 工具、hermes cron 或 /cron——它们会通过受支持的 API 更新任务存储。其他 Hermes 控制文件也一样,写入安全机制会拦下直接编辑。
受监管网关的生命周期限制
终端工具还有一道独立且不可覆盖的护栏:禁止从网关自己的受监管进程内部去停止或重启网关。因为自我重启可能在工具执行完成前就把它干掉,进而引发“监管器 / 自动恢复”死循环。用户审批、YOLO 模式、force=True 都不能绕过这道护栏。
这道护栏还会拦下针对网关所跑解释器镜像的“杀进程”命令——taskkill /F /IM python.exe、Stop-Process -Name python、pkill -9 python3、killall python、pkill -f python,以及 pgrep python | xargs kill 这类按名字推导出来的杀法。原因很简单:受监管的网关本身就是个 python 进程,这么一杀连 Agent 自己这一轮都没了。只针对 Agent 自己拥有的进程的杀法可以放行:后台任务的 proc_* id(process(action="kill", …))或明确的 PID(taskkill /F /PID <pid>、kill <pid>)。其他镜像名(比如 taskkill /F /IM notepad.exe)不受影响。这道护栏在所有生成的启动器下都生效——systemd unit、launchd plist、s6 run script 和 Windows 计划任务,靠它们导出的 HERMES_SUPERVISED_CHILD 标记来识别。
在 macOS 上,实际执行的 launchctl submit 和 launchctl bootstrap 命令会被限制,不管 job label 是什么。这是一条保守的注册限制,目的是拦住那些用中性 label 的间接重启助手,而不是去检查目标 plist。它也会拒绝那些 RunAtLoad=false 且没有 KeepAlive 键的独立计划任务;被拒绝并不意味着该任务一定用了 KeepAlive 或控制了 Hermes。
如果确实需要维护 LaunchAgent,请在网关之外单独开一个 shell 操作。某些独立的 load/unload 命令目前能通过基于 label 的检查,但这并不是经过目标验证的豁免,也不是绕过 bootstrap 拒绝的受支持方式。只读的 launchctl print 不属于生命周期操作。外部维护之后,要把磁盘上的 plist 和已加载的 job 区分开:先校验 plist,再读回已加载的计划,然后才能报告“已激活”。
工具拒绝意味着该命令没有通过那次工具调用执行;助手自己决定不发这个调用,是另一回事(模型决策),换个模型也不会改变终端护栏的策略。
用户自定义拒绝规则(approvals.deny)
硬性黑名单是代码里写死的,approvals.deny 则是它的“用户可编辑版”:一串 glob 模式,匹配到的终端命令会被无条件拦截——在 --yolo、/yolo、approvals.mode: off 生效之前就拦下。想“除了这几件事,其他随便干”的话,就用它。
approvals:
deny:
- "git push --force*"
- "*curl*|*sh*"
- "dd if=* of=/dev/*"
几个细节:
- 模式是 fnmatch glob(
*、?、[...]),大小写不敏感,匹配整条命令文本,也匹配单个可执行命令候选。git push --force*能匹配git push --force origin main,但匹配不到git push origin main。 - 匹配跑在危险模式检测器用的同一套“规范化 / 去混淆”命令变体上,所以
git pu""sh --force这种简单引号把戏躲不过去。 - 可执行候选既保留字面路径,也匹配它的 basename:
sudo *覆盖/usr/bin/sudo -n id和./sudo -n id;而/usr/bin/sudo *这种路径专属规则不会变成对所有叫sudo的二进制都生效。 - 引号感知的解析能揪出赋值、前置重定向、
;、&&、||、管道、分组、命令替换,以及普通if/then/else/do转换之后的命令。支持的启动器包括sudo、env、command、exec、nohup、setsid、time、nice、timeout、stdbuf、ionice、chrt、taskset、chroot。已知的选项操作数会被跳过;command -v/-V查询不算执行。shell-c载荷会被递归检查。env -S/--split-string里的字面“可执行文件 + 参数”字符串按 GNU 引号和转义规则处理(包括\_词边界和\c终止),其余命令参数追加在后面;这些参数里的 shell 标点保持为数据,除非真的被某个 shell-c消费。env -a/--argv0的值是参数,不是可执行文件名。shell 和 GNU split-string 的注释不会引入可执行候选。 - 在额外的可执行候选中,词与词之间的空白会被折叠,但引号内的参数内容和参数路径会保留。所以像
git status这样的精确规则,也能匹配env git\tstatus; echo done(\t代表制表符)。而echo 'sudo -n id'这种引号里的“提及”不会被提升为命令。已有的整条输入 glob(比如*sudo*)仍然会有意匹配这类提及。 - YAML 引号:模式一定要加引号。裸的开头
*会被 YAML 当成别名解析失败,{、!、:也各有含义,单引号对 shell 风格内容最安全。 - 拒绝规则对所有终端后端都生效,包括隔离容器,且先于任何后端专属的审批捷径。被拦下的命令会给 Agent 返回一个 BLOCKED 错误,告诉它别重试、别换说法,什么都不会执行。
- 和审批配置的其他部分一样,改完立即生效(配置缓存按 mtime 判断),不用重启会话。
沙箱镜像与基础设施密钥
如果你用容器沙箱,官方给的桌面沙箱镜像是:
docker_image: "nousresearch/hermes-sandbox:desktop"
另外有一条硬规矩要记住:Hermes 自己的基础设施密钥(provider API key、网关 token 之类)绝对不能塞进 env_passthrough。它们有专门的传递机制,不走这条路。你声明这类名字时会被直接拒绝;就算你先声明了、之后某个平台适配器(比如技能加载完之后才注册的插件适配器)把它认领了,从那一刻起它也会停止被转发。
TLS 证书信任
Hermes 通过 truststore 初始化平台验证器:Windows 走系统证书存储,macOS 走系统信任服务,Linux 走 OpenSSL 系统信任路径。初始化失败时会记日志并回退到 OpenSSL 默认值。
如果你在公司 TLS 代理后面,请按组织的操作系统信任流程安装其根证书。Hermes 的 provider 解析器不再通过 HERMES_CA_BUNDLE 或那套旧的 CA 环境变量阶梯来选择信任。沙箱子进程可以有自己的独立 CA 配置。原先的启动证书守卫也一并去掉了:Hermes 不再校验 HERMES_CA_BUNDLE / SSL_CERT_FILE / REQUESTS_CA_BUNDLE 这些变量。
小结
安全这玩意儿,从来不是单点防御,而是层层设防。Hermes 的八层模型把“人、机、环境”都考虑到了:人负责审批关键操作,机器负责扫描和隔离,环境负责容器和路径加固。对于普通用户,保持默认的 smart 模式就够用;真要开 YOLO,记得自己心里有点数。
📖 官方文档
本文根据 Hermes Agent 官方文档编写,原文见:官方文档 › user-guide/security