正文

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 里的 COPYADD 可以引用这个上下文里的文件。Docker 官方文档也说明,本地目录作为 context 时,这些文件会对 builder 可用。

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

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

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

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

.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
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

把 .dockerignore 当成第一道边界

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

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

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

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

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/

不要用 ARG / ENV 传密钥

很多 Dockerfile 会这样写:

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

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

构建时传入 secret:

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

构建时传 SSH agent:

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

dockerfile
ARG NPM_TOKEN
ENV DATABASE_URL="postgres://user:pass@example/db"
RUN npm ci
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
PowerShell
docker buildx build --secret id=npmrc,src=$HOME\.npmrc -t my-app:local .
dockerfile
# syntax=docker/dockerfile:1
FROM alpine/git AS source
RUN --mount=type=ssh git clone git@github.com:org/private-repo.git /src
PowerShell
docker buildx build --ssh default -t my-app:local .

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

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

构建后先看历史:

重点查这些内容:

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

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

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

ARG NPM_TOKEN改成 BuildKit secret mount
ENV DATABASE_URL=...改成运行时环境变量或 secret manager
RUN echo token=...立即轮换 token,重建镜像
COPY . .缩小 COPY 范围,补 .dockerignore
RUN rm .env不算修复,secret 可能在前一层
PowerShell
docker history --no-trunc my-app:local
PowerShell
docker save my-app:local -o image.tar
tar -tf image.tar | Select-String -Pattern "layer.tar|json"
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

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

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

更安全的结构:

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

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

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

  • 源码里的测试目录。
  • 包管理器缓存。
  • .git
  • 构建时 .npmrc
  • 本地 .env
  • 数据库 dump。
  • 调试脚本和内部文档。
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
dockerfile
FROM nginx:1.29-alpine
COPY --from=build /app /usr/share/nginx/html

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

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

安全做法:

清理本地 builder cache:

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

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

  • GitHub Actions / GitLab CI cache。
  • registry cache tag。
  • buildx remote builder。
  • Docker Build Cloud 或企业远程构建环境。
  • 已推送的旧 image tag。
私有依赖下载用 secret mount,不用 ARG
SSH 拉代码用 SSH mount,不复制私钥
CI 远程缓存不缓存接触过 secret 的目录
发现泄露后重建--no-cache,必要时换 builder
基础镜像更新--pull 获取新基础镜像
PowerShell
docker builder prune

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

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

不要这样:

更稳的做法是:

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

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

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"]
PowerShell
docker run --env-file .env.runtime my-app:local

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

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

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

.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发现泄露后旧镜像已撤回或禁用
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。按泄露处理。

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

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

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

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

参考来源

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 分钟

作者信息