正文

Docker 镜像泄露 secret,很多时候不是黑客攻破了服务器,而是开发者把本地文件、构建参数、缓存层或调试脚本一起打进了镜像。镜像一旦推到 registry、被 CI 缓存、被同事拉取,里面的 .env、SSH key、npm token、云厂商凭据就不再只是“本机误操作”。

2023 年一项针对容器镜像的互联网范围研究分析了 337,171 个 Docker Hub 镜像和 8,076 个其它私有 registry 镜像,发现 8.5% 的镜像包含 secret,并识别出 52,107 个私钥和 3,158 个 API secret。研究还提到,一部分泄露密钥已经在真实 TLS 或 SSH 主机上被使用。参考:Secrets Revealed in Container Images。

上线前先把 Docker 镜像当成一个会被复制、缓存、下载、解包和长期保存的制品。你要检查的不只是最终容器能不能启动,还要确认构建上下文、镜像层、镜像历史、缓存和 CI 日志里没有凭据。

最容易泄露的位置常见内容
build context.env、测试数据、数据库 dump、私钥、日志
ARG / ENVAPI key、npm token、云厂商凭据
COPY . .整个仓库、隐藏目录、临时输出
构建缓存下载私有包时用过的 token、生成文件
镜像历史构建命令里的明文参数
多阶段构建从 build stage 误拷贝了配置和缓存
CI 日志docker build 命令、构建输出、错误堆栈

先查 build context 里有什么

Docker 构建不是只读 Dockerfile。你执行 docker build . 时,当前目录会作为 build context,Dockerfile 里的 COPY、ADD 可以引用这个上下文里的文件。Docker 官方文档也说明,本地目录作为 context 时,这些文件会对 builder 可用。

先列出项目里不应该进入构建上下文的文件:

PowerShell
rg -n --hidden -g "!node_modules" -g "!dist" -g "!build" -g "!.output" "(PASSWORD|SECRET|TOKEN|PRIVATE_KEY|DATABASE_URL|AWS_|GCP_|AZURE_|OPENAI_API_KEY|ANTHROPIC_API_KEY)" .

再查容易被忽略的文件名:

PowerShell
Get-ChildItem -Force -Recurse -File |
  Where-Object {
    $_.Name -match '(^\.env|\.pem$|\.key$|id_rsa|id_ed25519|credentials|secret|dump|backup|\.sqlite$|\.db$)'
  } |
  Select-Object FullName

这些文件默认不要进 build context:

文件风险
.env / .env.production生产数据库、API key、JWT secret
*.pem / *.keyTLS 私钥、SSH 私钥
.npmrc / .yarnrc.yml私有 registry token
.aws/ / .config/gcloud/云厂商凭据
db.dump / *.sqlite / *.db真实业务数据
logs/ / coverage/ / tmp/请求、token、用户样本
.git/历史提交和可能被删掉的旧 secret

不要只看 Dockerfile 有没有 COPY . .。只要文件在 context 里,后续 Dockerfile、脚本、构建工具或误配置就可能把它带进镜像。

把 .dockerignore 当成第一道边界

.dockerignore 不是为了让构建快一点而已。它是防止本地敏感文件进入 builder 的第一道边界。Docker 文档说明,构建客户端会在发送 build context 前按 .dockerignore 排除匹配文件。

一个基础版本可以这样写:

Text
# secrets
.env
.env.*
*.pem
*.key
id_rsa
id_ed25519
secrets/
credentials/

# local and VCS
.git
.svn
.hg
.DS_Store

# dependencies and build output
node_modules/
dist/
build/
.output/
.next/
coverage/
.cache/

# data and logs
*.log
logs/
tmp/
*.sqlite
*.db
*.dump
backup/

如果项目里有多个 Dockerfile,Docker 还支持 Dockerfile-specific ignore 文件,例如 build.Dockerfile.dockerignore。这适合把测试镜像、lint 镜像、生产镜像分开处理:测试镜像可以包含测试夹具,生产镜像不应该包含。

注意 .dockerignore 不是 Git ignore。.gitignore 里排除的文件,仍然可能被 Docker build context 发送出去。两份文件要分别维护。

不要用 ARG / ENV 传密钥

很多 Dockerfile 会这样写:

dockerfile
ARG NPM_TOKEN
ENV DATABASE_URL="postgres://user:pass@example/db"
RUN npm ci

这很危险。Docker 的 Build secrets 文档明确说,build arguments 和 environment variables 不适合传 build secret,因为它们会持久化在最终镜像里。Docker 的 SecretsUsedInArgOrEnv build check 也把“在 ARG 或 ENV 里使用敏感数据”标成问题。

如果构建时必须访问私有 npm、GitHub Packages、私有 PyPI 或云对象存储,用 BuildKit secret mount:

dockerfile
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc pnpm install --frozen-lockfile

构建时传入 secret:

PowerShell
docker buildx build --secret id=npmrc,src=$HOME\.npmrc -t my-app:local .

如果需要拉私有 Git 仓库,用 SSH mount,不要把私钥复制进镜像:

dockerfile
# syntax=docker/dockerfile:1
FROM alpine/git AS source
RUN --mount=type=ssh git clone git@github.com:org/private-repo.git /src

构建时传 SSH agent:

PowerShell
docker buildx build --ssh default -t my-app:local .

成功标准不是“构建能拉到私有包”,而是构建完成后镜像里没有 .npmrc、私钥、token 和数据库连接串。

检查镜像历史里有没有明文

镜像层和历史记录会保留很多构建痕迹。即使你在后续层里 rm .env,前面的层也可能已经包含过它。

构建后先看历史:

PowerShell
docker history --no-trunc my-app:local

重点查这些内容:

历史内容处理方式
ARG NPM_TOKEN改成 BuildKit secret mount
ENV DATABASE_URL=...改成运行时环境变量或 secret manager
RUN echo token=...立即轮换 token,重建镜像
COPY . .缩小 COPY 范围,补 .dockerignore
RUN rm .env不算修复,secret 可能在前一层

再导出镜像做一次字符串扫描:

PowerShell
docker save my-app:local -o image.tar
tar -tf image.tar | Select-String -Pattern "layer.tar|json"

如果本机有 rg,可以对导出的 tar 做粗查:

PowerShell
rg -a -n "(DATABASE_URL|PRIVATE_KEY|BEGIN RSA PRIVATE KEY|BEGIN OPENSSH PRIVATE KEY|NPM_TOKEN|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY)" image.tar

这不是完整安全扫描,但能快速抓到最明显的泄露。命中任何真实 secret,都按泄露处理:撤销、轮换、重建、删除旧镜像 tag,并检查 registry 和 CI 缓存。

多阶段构建只拷贝运行时需要的文件

多阶段构建的价值不是“Dockerfile 看起来高级”,而是把编译工具、依赖缓存、测试文件、源码和构建 secret 留在 build stage。Docker 官方文档说明,多阶段构建可以从一个 stage 选择性复制 artifact 到另一个 stage,留下最终镜像不需要的内容。

更安全的结构:

dockerfile
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc pnpm install --frozen-lockfile

FROM deps AS build
WORKDIR /app
COPY src ./src
COPY public ./public
COPY package.json tsconfig.json vite.config.ts ./
RUN pnpm build

FROM nginx:1.29-alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html

不要在 runtime stage 里复制整个 build 目录:

dockerfile
FROM nginx:1.29-alpine
COPY --from=build /app /usr/share/nginx/html

后一种写法可能把源码、测试、.npmrc、缓存、临时文件一起带进最终镜像。

后端应用也一样。最终镜像只需要运行产物、生产依赖和必要配置,不需要:

  • 源码里的测试目录。
  • 包管理器缓存。
  • .git。
  • 构建时 .npmrc。
  • 本地 .env。
  • 数据库 dump。
  • 调试脚本和内部文档。

构建缓存不要接触长期凭据

构建缓存是提速工具,不是 secret 存储。Docker build cache 会复用之前构建产生的层;如果某一层下载私有依赖时接触过 secret,后续缓存、远程 builder、CI cache 和 registry cache 都可能让排查变复杂。

安全做法:

场景建议
私有依赖下载用 secret mount,不用 ARG
SSH 拉代码用 SSH mount,不复制私钥
CI 远程缓存不缓存接触过 secret 的目录
发现泄露后重建用 --no-cache,必要时换 builder
基础镜像更新用 --pull 获取新基础镜像

清理本地 builder cache:

PowerShell
docker builder prune

如果泄露发生在 CI 或远程 builder,不要只清理本机。要检查:

  1. GitHub Actions / GitLab CI cache。
  2. registry cache tag。
  3. buildx remote builder。
  4. Docker Build Cloud 或企业远程构建环境。
  5. 已推送的旧 image tag。

只要 secret 可能进入过远程缓存,就按远程泄露处理。

不要把运行时配置烤进镜像

镜像应该尽量是可复用制品。数据库地址、JWT secret、第三方 API key、对象存储 key、OAuth client secret 都应该在运行时注入,而不是写进 Dockerfile。

不要这样:

dockerfile
ENV DATABASE_URL="postgres://prod-user:prod-pass@db.example.com/app"
ENV JWT_SECRET="change-me-in-prod"

更稳的做法是:

dockerfile
ENV NODE_ENV=production
CMD ["node", "server.js"]

运行时再由部署平台、Kubernetes Secret、Docker secret、云厂商 Secret Manager 或 CI/CD 的受控变量注入:

PowerShell
docker run --env-file .env.runtime my-app:local

.env.runtime 不要提交到仓库,也不要复制进镜像。它是运行时输入,不是镜像内容。

推送前跑一张镜像检查清单

准备推送镜像前,至少做这些检查。

检查项通过标准
.dockerignore已排除 .env、私钥、日志、dump、.git
Dockerfile没有用 ARG / ENV 传 secret
BuildKit私有包和私有仓库使用 secret / SSH mount
COPY不使用无边界的 COPY . . 进入 runtime stage
多阶段构建runtime 只包含运行产物和必要依赖
镜像历史docker history --no-trunc 没有明文凭据
镜像导出docker save 粗查没有 key、token、连接串
CI 日志构建命令和报错没有打印 secret
缓存接触过 secret 的缓存不会上传到远程
旧 tag发现泄露后旧镜像已撤回或禁用

可以把这段放进 PR 模板或发布清单:

Text
Docker image release check:
- [ ] .dockerignore excludes secrets, logs, dumps, .git and local env files
- [ ] Dockerfile does not use ARG / ENV for tokens or passwords
- [ ] Private package install uses BuildKit secret mount
- [ ] Private Git access uses SSH mount
- [ ] Runtime stage copies only required artifacts
- [ ] docker history --no-trunc was checked
- [ ] image archive was scanned for obvious secrets
- [ ] CI logs do not print secrets
- [ ] If any secret was exposed, it has been rotated

已经把 secret 打进镜像怎么办

如果镜像已经推送,先不要只改 Dockerfile。按泄露处理。

  1. 立即撤销或轮换相关 secret。
  2. 从 registry 删除或禁用受影响 image tag。
  3. 清理 CI cache、registry cache、远程 builder cache。
  4. 用新 secret 和干净 builder 重新构建镜像。
  5. 检查部署环境是否仍在拉旧 tag。
  6. 查 registry pull 记录、CI 日志和访问日志。
  7. 把 .dockerignore、BuildKit secret mount、多阶段构建规则补进仓库。

如果泄露的是生产数据库、云厂商 root key、GitHub PAT、npm token 或 SSH 私钥,不要只轮换应用配置。还要检查是否有人用它访问过资源。

Docker 镜像是部署制品,也是可解包、可缓存、可复制的文件。把 .env、构建缓存、SSH key、npm token 和生产配置放进去,风险不会随着容器停止而消失。

上线前把镜像当成公开制品来检查:build context 先收窄,secret 用 mount,runtime stage 只放运行产物,推送前看历史和导出内容。做到这些,Docker 才是交付工具,而不是密钥打包工具。

参考来源

Build secretsDocker DocsBuild contextDocker DocsMulti-stage buildsDocker DocsSecretsUsedInArgOrEnvDocker DocsBuilding best practicesDocker DocsSecrets Revealed in Container ImagesarXiv

相关文章

Vite 7 升级卡住:为什么要求 Node 20.19+ / 22.12+工程实践 / 约 15 分钟AI Agent 让你安装依赖时,哪些包不能直接允许智能编程 / 约 12 分钟Vibe Coding 小应用上线前要查哪些安全坑智能编程 / 约 12 分钟AI Agent 权限怎么设:哪些命令能自动跑,哪些必须人工确认智能编程 / 约 13 分钟Claude Code 配置指南:先把这 7 件事配好智能编程 / 约 18 分钟AI Agent 已经能参与攻击链:Claude / Codex 使用前要限制哪些权限智能编程 / 约 10 分钟AI Coding 的下一步:从 prompt 技巧到工程约束智能编程 / 约 18 分钟Codex CLI 实用配置指南:先把这 6 件事配好,再开始让它写代码智能编程 / 约 18 分钟

作者信息