你导出的 profile 里,藏着这台机器的云凭据


你花了一上午把 profile 调顺手,然后打包发给同事:hermes profile export work -o work.tar.gz,附一句“解压就能复现我的环境”。同事照做了,也确实复现了你的环境——顺手还拿到了你的私有镜像仓库登录凭据。tar.gz 里有 skills/、config.yaml、state.db,也有一个你完全没注意到的 .docker/config.json。

这不是一个假设的漏洞,而是 hermes profile export 长期存在的真实行为:它排除了一部分凭据文件,但漏掉了操作系统级的凭据目录。官方在 10 月 3 日和 10 月 4 日连发两个 PR(#132201、#132654)把它彻底堵上。这篇文章讲清楚它漏了什么、为什么漏、以及你现在应该检查什么。

第一阶段:先堵 .ssh / .aws / .gnupg / .kube

更早的 #130525 已经让导出不含 auth.json、.env、OAuth token 这类 Hermes 自己的凭据。但 PROFILE_CREDENTIAL_PATHS 只覆盖了 subprocess 的 home/ 子树,操作系统凭据目录完全不在名单里。

于是 #132201 补上了第一批:.ssh、.aws、.gnupg、.kube,以及 direnv 的 .envrc。同时修掉两个相关问题:

  • 嵌套也要拦。 一个从真实 home 复制过来的 skill 目录里,可能带着 skills/some-skill/.ssh/id_rsa。旧的导出过滤器只匹配顶层名字,嵌套的 .ssh 照样被打包。修复后这些目录在任意深度都会被丢弃。
  • distribution 安装不能反向写入。 装一个 profile distribution 时,这些路径被标记为用户所有(profile_path_is_private),安装包不能再覆盖或植入它们。

第二阶段:再堵 .docker / .azure / .config/gh / .config/gcloud

#132201 上线一天后,#132654 发现同类目录还剩四个没被覆盖——而这四个恰恰是 agent/file_safety.py 早就禁止写入的目录。也就是说,Hermes 不允许写它们,却允许把它们导出:

目录 里面通常是什么
.docker/config.json 私有镜像仓库的 registry 认证
.azure/accessTokens.json Azure CLI 的访问令牌
.config/gh/hosts.yml GitHub CLI 的 token
.config/gcloud/ Google Cloud ADC 与 credentials.db

更麻烦的是 @file 上下文引用:在消息里写 @file:~/.config/gcloud/application_default_credentials.json,旧版本会把内容展开进上下文。而 gateway 会把不受信任的远端文本送进 @file 展开流程——属于同一类泄漏面。

为什么会漏:三份手抄的名单

根因不是某个目录被忘了,而是同一份名单被抄了三遍,然后各自漂移:

  1. agent/file_safety.py 里的写入拒绝名单;
  2. hermes_cli/profiles.py 里的导出过滤器;
  3. agent/context_references.py 里 @file 的拒绝名单。

修复的做法是让名单只有一个来源。agent/file_safety.py 现在有:

HOME_CREDENTIAL_DIRS = (".ssh", ".aws", ".gnupg", ".kube",
                        ".docker", ".azure", ".config/gh", ".config/gcloud")

写入拒绝前缀(build_write_denied_prefixes)从它派生;hermes_cli/profiles.py 里 _OS_CREDENTIAL_STORES 直接解包这个元组,于是导出名单(PROFILE_CREDENTIAL_PATHS、_EXPORT_CREDENTIAL_FILES)和写入拒绝名单不可能再对不上;agent/context_references.py 也直接 import 它,@file 的拦截范围因此多出了 .config/gcloud。

导出过滤本身换了一种匹配方式——按路径的尾部组件匹配,而不是只看顶层:

def _export_credential_entries(directory: str, contents: list) -> set:
    """Entries of *directory* that are an _EXPORT_CREDENTIAL_FILES store: matched on trailing path
    components, so ``.config/gh`` drops at any depth while the rest of ``.config`` ships."""

所以 .config/gh 和 .config/gcloud 现在在任意深度都会被丢掉,而 .config/other、.config/ghost、.config/gh.bak 这类无关内容仍然正常导出。

已经导出过?先当它泄漏了

这次修复只影响修复之后的导出。如果你之前把 profile 归档发给过别人、传过网盘、或者提交进过仓库,正确的处理是轮换,而不是删掉那个文件:

  1. 先看清楚归档里到底有什么(tar.gz 可以直接列目录,不需要解压):
tar -tzf work.tar.gz | grep -E '(^|/)\.(ssh|aws|gnupg|kube|docker|azure|envrc)/|\.config/(gh|gcloud)/'
  1. 命中任何一项,就把对应的凭据当作已暴露处理:换 SSH key、轮换 AWS / Azure / gcloud 凭据、吊销并重签 GitHub token、改私有 registry 的密码。
  2. 重新导出并确认干净(同样的 grep 应该无输出),再发出去。

小结

  • hermes profile export 现在是 tar.gz 归档,导出时会排除 Hermes 自己的凭据文件和 8 个 home 级凭据目录;
  • 遗漏的原因是三份手抄名单漂移,修法是单一来源 + 尾部路径匹配;
  • @file 展开和 distribution 安装走的是同一份名单,一起收紧了;
  • 修复只保护之后的导出,历史归档需要你自己轮换凭据。

想了解更多 profile 侧的隔离设计,可以看 凭据脱敏的三条外发通道 和 profile 改名与身份切换;hermes profile 的完整子命令见命令参考。