容器里那几百个 CVE,其实不是你的代码惹的祸

2026-06-05 34 预计阅读时间:1 分钟
来源:docker.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

安全团队第一次对容器环境做漏洞扫描时,通常会看到一个让人头皮发麻的数字——几百个已知 CVE。逐个排查后会发现一个反直觉的事实:绝大多数漏洞根本不在你的应用代码里,而是来自基础镜像自带的那些你从未调用过的包:shell、编译器、调试工具、多余的库。换句话说,你辛辛苦苦写的业务代码只占攻击面的极小一部分,真正的风险藏在供应链上游。

这就引出了 Hardened Image(加固镜像) 的核心思路:把基础镜像里不需要的东西砍掉,让攻击面从"几百个 CVE"收缩到"只有应用真正依赖的那几个"。

CVE 的真正来源:基础镜像的"全家桶"

普通基础镜像为了通用性,预装了大量工具。以 ubuntu:22.04 为例,默认带 bash、apt、dpkg、gcc 运行时依赖、netcat、甚至 python3——你的 Go 二进制或 Java 应用压根不需要它们,但它们每一个都可能携带 CVE。

扫描一个典型未加固镜像的结果大致是这样的分布:

来源 CVE 占比 典型组件
基础镜像预装包 70-90% bash, libssl, libcurl, gcc-libs, python3
应用直接依赖 10-20% 框架/SDK 自带库
应用自身代码 <5% 业务逻辑漏洞

这意味着:哪怕你的代码写得滴水不漏,只要基础镜像没加固,攻击面就已经敞开了。攻击者不需要找到你代码的 0day,只需要利用镜像里那个你从未用过但一直存在的 libcurl 远程执行漏洞就够了。

加固镜像做了什么

Hardened Image 的策略可以概括为三条:

1. 切换到最小化基础镜像

ubuntu / debian 切换到 distrolessalpine。Distroless 镜像只包含你的应用及其运行时依赖,没有 shell、没有包管理器、没有调试工具——连 apkapt 都不存在,攻击者即使拿到容器权限也无法轻易安装新工具。

2. 移除不需要的包和文件

如果必须用较大的基础镜像,在构建阶段显式删除不必要组件:文档、man page、缓存、开发工具链。每删一个包,就少了一整类 CVE。

3. 固化运行时行为

只读根文件系统、非 root 用户运行、删除写权限目录——即使有残留漏洞,也大幅降低可利用性。

实战:从普通镜像到加固镜像

下面给出三种常见加固路径,从轻到重,你可以按团队现状选择。

路径一:Alpine 替换 + 非 root 用户

最简单的加固:把基础镜像换成 Alpine,并用非 root 用户运行。

# --- before: 普通镜像 ---
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 python3-pip
COPY . /app
WORKDIR /app
RUN pip3 install -r requirements.txt
CMD ["python3", "app.py"]

# --- after: 加固镜像 ---
FROM python:3.12-alpine

# 只安装运行时真正需要的系统库(示例:PostgreSQL 客户端库)
RUN apk add --no-cache libpq \
    && rm -rf /var/cache/apk/*

# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

COPY requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip install --no-cache-dir -r requirements.txt \
    && rm -rf /root/.cache

COPY . /app
USER appuser

CMD ["python", "app.py"]

关键改动说明: - --no-cache 让 apk 不保留缓存索引;rm -rf /var/cache/apk* 双重保险 - pip install --no-cache-dir 不在镜像内留 pip 缓存 - USER appuser 确保进程不以 root 运行

路径二:Distroless——连 shell 都没有

Google 的 distroless 镜像更激进:镜像里只有你的应用二进制和它的运行时依赖,没有 /bin/sh,没有 apk/apt,没有任何交互入口。

# --- 多阶段构建 + distroless ---
FROM python:3.12-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

COPY . /app

# --- 运行阶段:distroless,无 shell ---
FROM gcr.io/distroless/python3-debian12

COPY --from=builder /install /usr/local
COPY --from=builder /app /app

WORKDIR /app
CMD ["app.py"]

注意:CMD 只能写数组形式(["app.py"]),不能用 shell 语法("python app.py"),因为镜像里根本没有 shell。调试时你可以临时用 debug 版本镜像(gcr.io/distroless/python3-debian12:debug),它带一个 busybox shell,但生产环境绝不应该用 debug 版本。

路径三:只读根文件系统 + 安全上下文

在 Kubernetes 中配合只读根文件系统,进一步锁死运行时:

apiVersion: v1
kind: Pod
metadata:
  name: hardened-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 1000
  containers:
    - name: app
      image: my-registry/hardened-app:latest
      securityContext:
        readOnlyRootFilesystem: true
        allowPrivilegeEscalation: false
        capabilities:
          drop:
            - ALL
      volumeMounts:
        # 只读根文件系统下,需要把写目录挂成 tmpfs
        - name: tmp-write
          mountPath: /tmp
        - name: cache-write
          mountPath: /app/.cache
  volumes:
    - name: tmp-write
      emptyDir:
        medium: Memory          # tmpfs,内存临时存储
    - name: cache-write
      emptyDir:
        medium: Memory

这段 YAML 做了四件事: - runAsNonRoot: true 强制非 root,Pod 级别兜底 - readOnlyRootFilesystem: true 根文件系统只读,攻击者无法写入恶意文件 - drop ALL capabilities 移除所有 Linux capabilities,只留最小权限 - 写目录用 emptyDir + Memory(tmpfs)挂载,重启后自动清空

验证加固效果:扫描对比

加固前后用 Grype 或 Trivy 扫描,量化差异:

# 安装 Grype(Anchore 的开源扫描器)
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

# 扫描普通镜像
grype ubuntu:22.04 -o table | head -30

# 扫描加固后的镜像
grype my-registry/hardened-app:latest -o table | head -30

# 只看严重/高危 CVE 数量对比
grype ubuntu:22.04 -o json | jq '.matches | length'
grype my-registry/hardened-app:latest -o json | jq '.matches | length'

典型结果:ubuntu:22.04 可能检出 200+ CVE,而加固后的 distroless/alpine 镜像通常只剩 0-10 个,且多数是运行时库的低危问题。

采用加固镜像的现实考量

加固镜像不是零成本的,团队需要评估以下取舍:

调试成本上升。 Distroless 镜像没有 shell,kubectl exec 进去只能看到空荡荡的文件系统。应对方式:本地开发用普通镜像调试,生产用加固镜像;紧急排查时临时部署 debug 版本。

CI/CD 流程需要调整。 多阶段构建让 Dockerfile 更复杂;只读根文件系统要求应用不能随意写文件,日志要输出到 stdout、临时数据要用 tmpfs 或外部存储。

基础镜像更新节奏。 Alpine 和 distroless 的更新频率低于 Ubuntu/Debian,关键 CVE 修复可能滞后几天。建议在 CI 中加入每日镜像扫描步骤,发现高危 CVE 时自动触发重建。

渐进式路线图:

阶段 动作 预期 CVE 降幅
第 1 周 所有镜像添加 USER appuser,禁止 root 运行 0%(降可利用性,不降 CVE 数)
第 2-3 周 基础镜像从 ubuntu/debian 切到 alpine 60-80%
第 4-6 周 关键服务迁移到 distroless + 多阶段构建 85-95%
第 7 周+ Kubernetes 加 readOnlyRootFilesystem + drop ALL capabilities 运行时攻击面进一步收窄

不要试图一步到位。先让所有镜像不以 root 运行——这一个改动就能阻断大量提权攻击路径。再逐步替换基础镜像、收紧文件系统权限。每一步都让攻击者的可用选项更少,而你的运维负担只增加一点点。


相关推荐