三年了,那个藏在 Docker 镜像里的 token 还在——今天一个开源工具用 25 分钟把它扒了出来
配了三年 Docker 构建,今天才知道一个五行的 Dockerfile 模式,会把你的 GitHub token 向整个互联网直播——而且没有任何告警,直到有人来把它捡走。
这件事是怎么被发现的
安全公司 Strix 在评估要不要把自家模型和数据交给 Baseten——一个估值 130 亿美元的 AI 推理平台——之前,他们先用自己的 AI 自动化渗透测试工具 Strix 扫描了一遍 Baseten 的基础设施。没有任何凭证,没有源码,只有一个域名 *.baseten.co。
大约 25 分钟后,工具找到一个公开可读的 Harbor 容器镜像仓库。匿名拉取了 baseten/baseten-app 镜像,运行开源工具 TruffleHog 扫描层信息,在镜像 config 的 history[].created_by 字段里找到一条 RUN 命令,其中 GITHUB_TOKEN 构建参数被完整展开了。
验证一下:TruffleHog 提取出来的 token 发一个只读请求到 GitHub API。返回 HTTP 200,账号 basetenbot,所属 basetenlabs 组织。
影响范围:主产品仓库的 admin + push 权限、flux-cd(GitOps 仓库,定义生产集群期望状态)的 admin + push 权限、Homebrew tap 的 admin + push 权限,外加若干客户私有仓库的读写权限。
这个 token 的镜像构建日期是 2023 年 3 月,到 2026 年 7 月被发现时,已经在公开的 Harbor 仓库里裸奔了三年多。
五行的 Dockerfile 为什么会让 token 泄露
问题不在 Dockerfile 的哪一行,而是 Docker 构建本身的一个基本特性:每一个 RUN/ARG/ENV 指令都会产生一个不可变的镜像层,而这一层的 created_by 字段会完整记录这条命令的原文。
常见的 GitHub token 用于私有依赖拉取的写法会把 token 作为 ARG 传入,命令类似:
ARG GITHUB_TOKEN
RUN git config --global --add url."https://${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:"
即使后来 RUN 命令里把文件删了、ENV 变量清理了,那条带明文 token 的命令仍然完整地留在层的 created_by 字段里。任何拿到这个镜像的人,都可以用 docker history --no-trunc 直接读出来。
正确做法是使用 BuildKit secret mounts,token 在构建时挂载到内存中的 tmpfs,不写入任何层:
# syntax=docker/dockerfile:1
FROM python:3.12-slim
RUN --mount=type=secret,id=github_token git config --global url."https://$(cat /run/secrets/github_token)@github.com/".insteadOf "git@github.com:"
构建命令变为:
DOCKER_BUILDKIT=1 docker build --secret id=github_token,src=${GITHUB_TOKEN_FILE} -t secure-image .
token 只在构建那一条 RUN 指令的运行期间存在于内存里,不进入任何镜像层,docker history 查不到,层内容里也没有。
三件事落地检查
1. 扫你现有的镜像
docker save myimage:latest | tar -xO | grep -i -E 'password|secret|token|key'
或者用 Trivy:
trivy image myimage:latest
如果有命中,基本修复路径是:把 ARG 改成 BuildKit secret mount,重新构建并加 --no-cache,然后立刻轮转所有流出的 token——因为你无法知道镜像被谁在什么时候拉走了。
2. 检查你的容器镜像仓库权限
本次事件的 Harbor 项目被设成了 public,导致任何人都可以匿名拉取镜像并分析层信息。不是所有的仓库都有这个配置,但默认值和有意识设 public 之间往往只差一个 checkbox。
3. 检查 token 权限和有效期
basetenbot 这个 token 拿了 repo 全权限,而它实际上只需要在构建阶段拉取依赖。把 token 权限收紧到最小范围,不会阻止这次的信息泄露,但能限制后续的暴露面。配合 OIDC 短效 token 效果更好——不需要长期有效 token,构建完自动失效。
25 分钟背后的新威胁逻辑
这件事最值得警觉的不只是 Baseten 的安全失误,而是攻击成本的变化。
以前找出一个公开镜像里的 GitHub token,需要懂 Docker 层结构、会用 TruffleHog、了解 registry 枚举,这一套技能组合只有安全研究员才有。但 Strix 演示的是:把这些步骤串起来的整个攻击链,现在可以由一个 autonomous agent 在 25 分钟内无干预完成,而且工具本身是免费开源的。
这意味着”我的服务太小/太不知名,不会有人来扫描”这个假设正在失效。当自动化攻击的成本降到趋近于零,任何公开暴露的凭证都会被迟早捡走。
Baseten 的安全团队在收到报告后响应很专业——当天锁了仓库项目,次日确认严重级别,下午 4 点前完成了 token 轮转,7 月 17 日关闭了所有剩余问题。但 token 的流转路径(进入镜像 → 进入公开仓库 → 被下载 → 存在层历史里)本身不会因为轮转而消除。修复构建过程,比轮转单一 token 更重要。
行动清单
docker history --no-trunc <your-image>查是否有 token/password/secret 出现在 CREATED BY 字段- 把
ARG GITHUB_TOKEN迁移到 BuildKit--mount=type=secret - 确认容器 registry 没有默认 public 项目
- 检查 GitHub token 权限范围是否超过构建实际需求
- 考虑为 CI/CD 引入 OIDC short-lived token 替代长期 static token
免费工具:TruffleHog(docker pull 后扫描层)+ Dive(交互式浏览镜像层)+ Trivy(CI/CD 集成扫描)
评论区
登录后可评论。