一句话总结
镜像是容器的"基因", 有什么漏洞, 跑什么用户, 暴露什么权限, 在 docker build 那一刻就决定了.
镜像安全是云原生安全的第一道防线: 构建阶段消灭的风险, 运行时就不需要防御.
容器镜像的攻击面
一个容器镜像从构建到运行, 每个环节都可能引入风险:
| 环节 | 风险 | 攻击示例 |
|---|---|---|
| 基础镜像 | 包含已知 CVE 漏洞的系统库 | 攻击者利用 OpenSSL 漏洞逃逸容器 |
| 依赖安装 | 引入恶意包或有漏洞的版本 | npm/pip 供应链投毒 |
| 构建过程 | 密钥泄露到镜像层 | git clone 用的 SSH key 留在中间层 |
| 镜像仓库 | 镜像被篡改或替换 | 中间人攻击替换 latest tag |
| 运行时用户 | 以 root 身份运行 | 应用漏洞直接获得宿主机 root |
| 多余组件 | 镜像中包含 shell, 编译器等 | 攻击者进入容器后可利用这些工具 |
镜像 = 打包好的行李箱.
把护照(密钥), 剪刀(危险工具), 过期食品(已知漏洞)都塞进去, 到了目的地(运行时)就是一堆安全隐患.
正确做法: 只带必需品, 行李箱上锁(签名), 过安检(扫描).
基础镜像选择
常见基础镜像对比
| 镜像 | 大小 | 包含内容 | 安全性 | 适用场景 |
|---|---|---|---|---|
ubuntu:22.04 |
~77MB | 完整 apt 包管理器 + 常用工具 | 攻击面大 | 开发调试, 需要系统工具 |
debian:slim |
~52MB | 精简 Debian, 无文档和非必需包 | 中等 | 需要 apt 但想减小体积 |
alpine:3.19 |
~7MB | musl libc + busybox + apk | 较好 | 通用轻量场景 |
gcr.io/distroless |
~2-20MB | 只有运行时依赖, 无 shell/包管理器 | 极好 | Go/Java/Node 生产运行 |
scratch |
0MB | 空镜像, 什么都没有 | 最佳 | 静态编译的 Go binary |
Go 服务的最佳实践
Go 可以静态编译(CGO_ENABLED=0), 产出一个不依赖任何系统库的单一 binary. 直接用 scratch 或 distroless/static 做基础镜像, 镜像里只有 binary, 没有 shell, 没有包管理器, 没有任何多余工具.
攻击者即使进入容器也无从下手.
Distroless 详解
Google 维护的 Distroless 镜像去掉了操作系统中一切非必需的东西:
- 没有 shell(/bin/sh, /bin/bash)
- 没有包管理器(apt, apk)
- 没有常用工具(curl, wget, ls)
- 只包含运行时所需的最小依赖(libc, ca-certificates 等)
|
Distroless 的调试困境
没有 shell 意味着不能 kubectl exec 进容器排查问题. 解决方案:
- 使用 Ephemeral Containers(K8s 1.25+ GA)临时注入调试容器
- 在 CI 中同时构建 debug 版本(基于 alpine)用于非生产环境
漏洞扫描
镜像中的系统包和应用依赖可能包含已知漏洞(CVE). 扫描器检查镜像的每一层, 对比 CVE 数据库, 报告风险.
Trivy: 全能扫描器
Trivy(Aqua Security 开源)是当前最流行的容器安全扫描工具:
|
Trivy 扫描结果示例:
|
CI 集成: 构建时拦截
|
扫描时机
构建时: CI 中扫描, 有 Critical 漏洞则阻止合并/部署.
仓库中: 镜像仓库(Harbor)定期重新扫描已存储的镜像(新 CVE 可能随时公布).
运行时: 准入控制器(如 Kyverno)拒绝部署未扫描或有高危漏洞的镜像.
三道防线缺一不可.
最小化镜像构建
多阶段构建
将"编译环境"和"运行环境"分离, 编译器, 源码, 构建工具不进入最终镜像:
|
减少层数与内容
- 合并 RUN 命令:
RUN apt install && apt clean && rm -rf /var/lib/apt/lists/*在一层中完成安装和清理 - .dockerignore: 排除 .git, node_modules, 测试文件等不需要的内容
- 固定版本:
FROM alpine:3.19.1而非alpine:latest, 确保构建可复现 - 不安装非必需包:
apt install --no-install-recommends
非 Root 运行
默认情况下容器以 root 运行, 如果应用被攻破, 攻击者直接获得 root 权限.
为什么危险
容器的 root 虽然通过 Linux Namespace 与宿主机隔离, 但:
- 如果有内核漏洞可以逃逸 Namespace → 直接变成宿主机 root
- 如果挂载了宿主机目录 → 可以修改宿主机文件
- 如果有 SYS_ADMIN capability → 可以挂载 proc 等特殊文件系统
实现非 root
|
|
runAsNonRoot 不检查 Dockerfile 的 USER 指令
runAsNonRoot: true 只在 Pod 启动时检查进程 UID 是否 ≠ 0. 如果镜像的 Dockerfile 没有设置 USER(默认 root), 但 K8s 配置了 runAsUser: 65532, 容器可以正常启动(K8s 覆盖了 UID).
如果只设 runAsNonRoot: true 而不指定 runAsUser, 且镜像 USER 是 root → Pod 启动失败.
镜像中的密钥泄露
Docker 镜像的每一层都是永久保存的. 即使在后面的层 rm 了一个文件, 它仍然在前面的层中可以被提取.
常见泄露方式
COPY .env /app/.env- 环境变量文件包含密钥RUN git clone https://token@github.com/...- token 留在层历史中COPY id_rsa /root/.ssh/id_rsa- SSH 私钥ARG DB_PASSWORD- 构建参数写入镜像元数据
防止泄露
|
检测已泄露的密钥
Trivy 和 docker history 可以扫描镜像层中的敏感内容. 还有专门的工具如 ggshield(GitGuardian), trufflehog 用于检测仓库和镜像中的 secret.
建议在 CI 中集成 secret 扫描作为必要检查.
镜像 Tag 与不可变性
latest 的风险
myapp:latest 是可变的, 今天指向 v1.2.3, 明天可能指向 v1.2.4. 这导致:
- 无法确定生产环境跑的到底是哪个版本
- 攻击者替换 latest tag → 部署恶意镜像
- 回滚时
kubectl rollout undo拉到的可能不是之前的版本
用 Digest 固定镜像
|
生产最佳实践
CI 构建时生成 image:git-sha 的 tag 并推送.
部署文件引用具体的 sha tag 或 digest.
通过 Kyverno 策略禁止使用 :latest tag 部署.
镜像仓库开启 tag 不可变(Immutable Tags)防止覆盖.
镜像仓库安全
- 私有仓库: 不要把生产镜像放在公开的 Docker Hub 上
- 漏洞扫描: Harbor 等企业仓库内置自动扫描
- RBAC: 控制谁能 push/pull 哪些镜像
- 镜像签名: Cosign 签名 + 准入策略验证(第 06 篇详解)
- 内容信任: Docker Content Trust / Notary 确保镜像未被篡改
镜像安全检查清单
Dockerfile 安全审查要点
每次 code review 时对照检查:
- □ 基础镜像使用固定版本 tag(非 latest)
- □ 使用多阶段构建, 编译工具不进入最终镜像
- □ 最终镜像尽量使用 distroless / scratch
- □ 设置了
USER为非 root - □ 没有在 COPY/ADD 中包含敏感文件
- □ 没有在 ARG/ENV 中硬编码密钥
- □ RUN 中用到密钥时使用
--mount=type=secret - □ .dockerignore 排除了 .git / .env / 测试文件
- □ CI 中集成了 Trivy 扫描(Critical/High 阻断)
- □ 部署文件使用具体版本 tag 或 digest
快速回顾
- 基础镜像选择: Go 用 scratch/distroless, 其他语言用 distroless 对应变体, 避免 ubuntu/debian 全量镜像
- 漏洞扫描: Trivy 集成到 CI, 构建 + 仓库 + 准入三道防线
- 多阶段构建: 编译环境和运行环境分离, 最终镜像只有 binary + 运行时依赖
- 非 root: Dockerfile USER + K8s runAsNonRoot + allowPrivilegeEscalation=false
- 密钥保护: BuildKit secret mount + 多阶段隔离, 绝不让密钥进入镜像层
- 不可变 tag: 使用 git-sha 或 digest, 禁止 latest
动手练习
- Trivy 扫描修复: 用 Trivy 扫描当前项目的 Docker 镜像, 列出所有 Critical/High 漏洞并修复(通常是升级基础镜像版本)
- 多阶段 + distroless 改写: 将一个基于
ubuntu的 Dockerfile 改写为多阶段 + distroless, 对比镜像大小和漏洞数量 - 验证镜像层密钥泄露: 在 Dockerfile 中故意
COPY一个 .env 文件, 然后用docker history和docker save | tar验证文件确实可以从镜像层中提取出来 - CI 自动扫描配置: 配置 GitHub Actions / GitLab CI 在构建后自动运行 Trivy, 设置 exit-code=1 让 Critical 漏洞阻断 pipeline