2. 容器镜像技术基础

要理解容器镜像安全,先要理解镜像“是什么、怎么存、怎么被运行”。容器镜像并不是一个巨大的单文件压缩包,而是由若干只读层叠加而成的文件系统快照,再配合一份描述配置的元数据。运行时把镜像拉取到本地,按层组装成容器可用的根文件系统,最后启动进程。镜像漏洞、层膨胀、重复层、基础镜像过期等问题,几乎都能从这套机制中找到根源。

一、镜像分层:内容寻址的只读层堆叠

镜像由一系列层(Layer)组成,每一层是一次文件系统变更的集合,例如“安装 nginx”“复制应用代码”“写入配置文件”。层本身是只读的,层与层之间通过联合挂载叠加,最终呈现为一个完整目录树。每个层用其内容的 SHA-256 摘要命名,这种内容寻址机制带来两个直接好处:相同内容的层只存一份,天然去重;摘要即校验值,任何篡改都会导致摘要变化。

镜像的顶层是一份 config 配置,记录架构、操作系统、环境变量、启动命令、暴露端口,以及按顺序排列的层摘要列表(rootfs.diff_ids)。最外层再由一个 manifest 清单把 config 与各层摘要绑定在一起。因此“镜像”实际是 manifest → config → layers 的引用链,而不是单一文件。

分层对安全治理的意义很实际:基础镜像层被多个业务镜像共享,一旦该层存在高危漏洞,影响面会成倍放大;反之,修复时只需替换受影响的那一层并重新构建,上层应用层可复用缓存。判断一个镜像是否“干净”,不能只看最终文件,还要看它引用了哪些层、这些层来自哪个基础镜像。

二、联合文件系统:把只读层拼成可写根目录

联合文件系统(UnionFS,如 overlay2、fuse-overlayfs)负责把多个只读层合并成一个统一视图。以 Linux 上最常见的 overlay2 为例,它把镜像层作为 lowerdir,容器启动时新建一个可写层作为 upperdir,再用 workdir 做原子操作中转,最终挂载出 merged 目录作为容器根文件系统。

# overlay2 挂载关系(示意)
lowerdir = /var/lib/docker/overlay2/<layerN>/diff : ... : <layer1>/diff
upperdir = /var/lib/docker/overlay2/<container>/diff
workdir  = /var/lib/docker/overlay2/<container>/work
merged   = /var/lib/docker/overlay2/<container>/merged

读写规则遵循 写时复制(Copy-on-Write):读文件时按层从上往下查找,命中即返回;修改文件时,先把该文件从只读层复制到可写层,再在可写层修改,原只读层保持不变。删除文件则在可写层生成 whiteout 标记,使上层“看不见”下层文件,但下层数据仍占用空间。

这解释了几个常见现象:容器内删除大文件,镜像体积不会变小,因为删除只发生在可写层;容器运行期间写入的数据都在可写层,容器销毁即丢失,需要持久化必须挂载 volume;同一镜像启动多个容器,只读层共享,只有可写层各自独立,因此启动快、内存占用低。

三、OCI 标准:镜像与运行时的共同契约

早期 Docker 私有格式导致镜像与运行时强绑定。开放容器倡议(OCI)定义了三个核心规范,使镜像可以在不同工具链之间自由流转:

  • OCI Image Spec:规定镜像的 manifest、config、层格式与媒体类型,是镜像的“存储与分发格式”。
  • OCI Runtime Spec:规定容器配置(config.json)与运行时行为,是容器“如何被启动”的契约。
  • OCI Distribution Spec:规定镜像仓库的 HTTP API,包括拉取、推送、清单查询等接口。

符合 OCI 的镜像可以被 containerd、CRI-O、Podman 等不同运行时消费;符合 OCI 的仓库可以被不同客户端访问。安全工具也因此受益:Trivy、Grype 等扫描器只需按 OCI 规范解析 manifest 与层,就能对任意来源的镜像做统一分析,而不必为每种私有格式单独适配。

四、镜像仓库与运行时的关系

镜像仓库(Registry)负责存储与分发,常见实现包括 Docker Hub、Harbor、云厂商 ACR/ECR 等。客户端通过 docker pull 或 ctr image pull 发起请求,仓库返回 manifest,客户端再按层摘要逐层下载并校验。运行时(如 containerd + runc)拿到本地镜像后,解压层、组装 rootfs、创建命名空间与 cgroup,最终启动容器进程。

下图展示了从镜像仓库到容器运行的完整链路,以及各环节对应的安全关注点:

镜像仓库 Registry manifest 清单 config 配置 layers 层数据 pull / 按摘要校验 本地镜像存储 layer1(基础镜像) layer2(依赖安装) layer3(应用代码) 联合挂载 运行时 Runtime lowerdir 只读层 upperdir 可写层 merged 根目录 runc 启动进程 namespace + cgroup 隔离 各环节安全关注点 仓库侧 镜像签名与来源验证 访问控制与私有仓库 manifest 篡改检测 镜像侧 基础镜像漏洞与过期 层膨胀与冗余依赖 敏感信息残留层 运行时侧 可写层数据泄露 权限与 capability 过大 挂载点逃逸风险 标准侧 OCI 规范一致性 跨工具链兼容性 扫描器解析准确性

五、从机制到治理:几个可操作的判断

理解了上述机制,就能把抽象的安全要求落到具体动作上。以下判断依据可直接用于日常镜像治理:

观察现象机制原因治理动作
容器内删文件后镜像仍很大删除只写 whiteout 到可写层,只读层数据仍在在构建阶段用多阶段构建,避免把临时文件写入最终层
多个镜像共享同一高危漏洞它们引用了相同的基础镜像层统一升级基础镜像并重建,而非逐个修补
扫描结果与仓库清单不一致本地缓存了旧 manifest 或旧层拉取时按摘要校验,必要时清理本地缓存后重拉
容器重启后配置丢失配置写在可写层,容器销毁即消失配置通过挂载 volume 或环境变量注入,不写入可写层
实践建议:在 CI 中固定使用镜像摘要(image@sha256:...)而非可变标签(如 latest),可确保每次部署的层内容完全一致,避免“同一标签、不同内容”带来的不可复现问题。