Hermes Agent 容器镜像安全实战:4 个阶段锁死基础镜像风险
【免费下载链接】hermes-agentThe agent that grows with you项目地址: https://gitcode.com/GitHub_Trending/he/hermes-agent
上周有用户反馈 Hermes Agent 容器里的行为跟上周对不上,翻构建日志,根因就一行:基础镜像的latest标签被上游悄悄更新了。那次复盘逼我们把 Hermes Agent 的容器镜像安全整条链路捋了一遍:基础镜像选型、镜像版本锁定、CI 更新流水线到运行时加固,文中每个动作都能直接在仓库里对着抄。
🔒 锁定基础镜像:别让 latest 替你选版本
latest标签为什么是定时炸弹
标签是指针,不是快照。你写FROM debian:latest,版本选择权就交给了上游,下一次构建可能比今天多一个补丁、少一个 CVE。麻烦在于:构建照样绿,问题要到生产才炸。所以基础镜像选型第一步是钉死具体版本,Hermes Agent 的 Dockerfile 里运行时镜像用的就是debian:13.4,版本写死,想升级只有你手动改才会变。换个说法:latest等于把"环境一致性"这条命交给了注册表的策略。
怎么把"锁版本"再锁一层
版本号还有一个口子:上游可能对同一个版本号重新打 tag,内容悄悄变了。真正的镜像版本锁定锁的是 digest。Hermes Agent 的 uv 来源阶段是这么写的:
FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543…digest 锁的是"这一串字节",不是"叫 0.11.6 的版本"。说白了,digest 是内容寻址,同一个 digest 永远指向同一组层;版本号是人起的名,人起的名会改。Node 来源阶段同样处理,将来升级只需换一行 digest,流水线其余部分不用动。
版本管"该是哪个",digest 管"到底是什么",两层都上才算锁死。
多阶段构建:把构建工具挡在运行时门外
锁版本是一方面,缩小攻击面是另一方面。Hermes Agent 把构建拆成多个阶段:sqlite_build阶段从源码编译指定版本的 SQLite(发行源里的包带着 WAL reset 的旧 bug),node_source阶段取 Node 二进制,运行时镜像只COPY --from拿最终产物。build-essential这类编译器和下载工具留在构建阶段,永远进不了最终镜像。一句话:不运行的东西,就是攻击者用不了的东西。
锁得再死,镜像也会老化——下一步把更新这件事交给流水线。
🔄 把更新写进流水线:三层分离的 CI 实践
怎么让 CI 自动追安全补丁
Hermes Agent 的 CI 更新流水线在 docker.yml,发布拆成三层,各干各的事:
| 层 | 职责 | 关键设计 |
|---|---|---|
| detect | 判断 PR 是否影响镜像 | tests-only 的 PR 跳过完整构建,省 45 分钟 |
| build | 双架构构建 + 集成测试 | 直接跑刚构建的:test镜像做 docker 测试 |
| publish | 推送并打 tag | 唯一持有注册表凭据的 job,测试全过才运行 |
这样"更新基础镜像"就变成:改 Dockerfile 里的版本或 digest,提 PR,CI 自动验证兼容性,没有"手动推了再说"的环节。publish 与 build 拆开还有个安全原因:PR 构建 job 跑的是外部提交的代码,绝不能给它注册表凭据。
拉第三方二进制,怎么防"上游换货"
构建过程还会从 release 下载 s6-overlay 这类组件,Hermes Agent 的规则是:下载不做校验就不许解包。Dockerfile 里这段逻辑核心就两行:
printf '%s %s\n' "${S6_SHA256}" /tmp/s6.tar.xz > /tmp/s6.sha256 sha256sum -c /tmp/s6.sha256任何一个 hash 不对,整条构建直接失败。sha256 值直接以 ARG 写在 Dockerfile 里,审计时对着上游 changelog 核一遍就行。这等于把"信任上游网站"换成了"信任仓库里这串字符"。此外还有 osv-scanner.yml 和 supply-chain-audit.yml 定期扫依赖漏洞。
流水线保证了"进来的干净",运行时加固则是容器镜像安全的最后一道门。
🛡️ 运行时加固:五步缩小攻击面
进程为什么必须以非 root 跑
容器里进程默认以 root 跑,进程一被攻破就是容器内最高权限。Hermes Agent 的做法是构建期建专用用户,拷贝时就把权限烙进去:
RUN useradd -u 10000 -m -d /opt/data hermes COPY --link --chmod=a+rX,go-w . .第一行建 UID 10000 的用户,用高位段是为了不跟宿主机的 uid 撞车;第二行让非 root 用户对代码目录只有读和执行,没有写。docker exec入口也堵上了:一个降权 shim 检测到 root 调用时会自动以hermes用户重新执行,避免你在/opt/data下写出 root 属主的孤儿文件。
只读文件系统之外,还能砍掉哪些东西
| 动作 | 做法 | 少掉的风险 |
|---|---|---|
| 代码目录只读 | /opt/hermes非 root 只读,可写状态集中到/opt/data | Agent 运行时改不了自己的 venv 把自己搞坏 |
| 缓存清零 | 装完依赖后清 APT 列表和 npm 缓存 | 攻击面与镜像体积一起下降 |
| 构建信息入镜 | HERMES_GIT_SHA构建参数写入.hermes_build_sha | 线上排障能精确到具体 commit |
| 只装生产依赖 | uv sync --frozen指定 extras,不用--all-extras | 开发/测试依赖不会顺带进生产镜像 |
--frozen这一步别省:它强制按仓库里提交的uv.lock解析,手改某个依赖版本不会在容器里静默生效。同理,npm 侧先拷贝清单再安装,锁文件一变才重新装,既省构建时间,也保证依赖可复现。
前面三个阶段走完,发布下一版镜像前,拿这张清单过一遍。
📋 落地自检清单
- 所有
FROM都有明确版本号或@sha256digest,不存在latest - 最终镜像里没有构建工具残留的编译器和下载器(
docker history检查) - 第三方下载全部经过 sha256 校验才解包
- 主进程以非 root 用户运行,且 exec 入口有降权兜底
- 只读与可写目录分离:代码只读,可写状态集中在一个 volume
- CI 在推送前先构建并跑集成测试,凭据只留在受保护的 job
- 构建参数烙入源码 commit,任意容器可回溯版本
- 依赖锁文件在仓库里,构建使用
--frozen
【免费下载链接】hermes-agentThe agent that grows with you项目地址: https://gitcode.com/GitHub_Trending/he/hermes-agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考