2、镜像安全:使用官方镜像、镜像签名验证、镜像漏洞扫描(Trivy)、最小化镜像构建

镜像安全,说白了就是容器安全的「第一道门」。你想想看,如果门本身就是坏的,后面做再多防护也没用。我在这个领域踩过不少坑,今天就把核心经验分享给你。

2.1 使用官方镜像:别贪图方便

我个人习惯,能选官方镜像就绝不选第三方。为什么?因为官方镜像有专人维护,漏洞修复也及时。我记得有一次,团队里有人图省事,直接从 Docker Hub 拉了个非官方的 Redis 镜像。结果呢?里面藏了个挖矿脚本。嗯,从那以后,我们团队就立了规矩:镜像来源必须可追溯

核心原则:

  • 优先选择 Docker Official Images(带「OFFICIAL」标签)
  • 其次选择 verified publisher(经过验证的发布者)
  • 避免使用个人上传的、没有维护记录的镜像

你可能会问:「那如果官方镜像版本太老怎么办?」我的建议是:自己基于官方镜像构建。比如官方 Ubuntu 镜像,你可以自己加一层,更新到最新补丁。这样既保证了基础安全,又满足了业务需求。

2.2 镜像签名验证:给镜像上个「身份证」

镜像签名验证,说白了就是确认「这个镜像确实是官方发布的,没被人动过手脚」。我刚开始接触 Docker 时,总觉得这步可有可无。直到有一次,我们生产环境被攻击了,原因就是有人篡改了镜像仓库里的镜像。

Docker 官方提供了 Docker Content Trust (DCT) 机制。启用后,你拉取的每个镜像都会验证签名。配置很简单:

# 启用 Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# 拉取镜像时会自动验证签名
docker pull nginx:latest

# 推送镜像时也会自动签名
docker push your-registry/your-image:tag

小技巧:我建议在 CI/CD 流水线里也开启 DCT。这样从构建到部署,全程都有签名验证。我曾经在 Jenkins 里加了一步签名检查,直接拦截了 3 次被篡改的镜像推送。

如果你用的是 Harbor 或其它企业级镜像仓库,它们通常也支持 NotaryCosign 等签名工具。我个人更推荐 Cosign,因为它轻量、易用,而且和 Kubernetes 集成得很好。

2.3 镜像漏洞扫描:用 Trivy 给镜像「体检」

镜像漏洞扫描,说白了就是给镜像做个全面体检。我常用的工具是 Trivy,它开源、快速、支持多种语言和操作系统。

为什么选 Trivy?因为它扫描速度快,而且误报率低。我记得有一次,用别的工具扫描,报了一堆漏洞,结果一查全是误报。换成 Trivy 后,清净多了。

安装和使用都很简单:

# 安装 Trivy(以 Linux 为例)
sudo apt-get install trivy

# 扫描本地镜像
trivy image nginx:latest

# 扫描结果会显示漏洞等级、CVE 编号、修复版本等
# 输出格式可以指定为 JSON、HTML 等
trivy image --format json nginx:latest

注意:扫描结果里的漏洞,不一定都要修。你要根据实际环境判断。比如一个漏洞只在特定条件下才能利用,而你的环境根本不满足那个条件,那就可以先放一放。我曾经见过有人把所有漏洞都修了,结果系统反而变得不稳定。

我建议把 Trivy 集成到 CI/CD 流水线里。每次构建镜像后自动扫描,如果发现高危漏洞,直接阻断构建。这样就能把问题扼杀在摇篮里。

2.4 最小化镜像构建:越轻越安全

最小化镜像构建,说白了就是「能少则少」。镜像越小,攻击面就越小,漏洞也越少。我见过有些人的镜像,动辄几个 G,里面装了一堆用不上的工具。你想想看,这些工具每一个都可能成为攻击者的突破口。

怎么做最小化?我总结了几个经验:

  • 使用 Alpine 基础镜像:Alpine 只有 5MB 左右,比 Ubuntu 小多了。但要注意,Alpine 用的是 musl libc,有些应用可能不兼容。
  • 多阶段构建:把构建环境和运行环境分开。构建环境里装编译器、依赖库,运行环境只保留最终产物。
  • 清理缓存和临时文件:apt-get 安装后记得清理缓存,npm install 后可以删掉 .npm 目录。

来看一个多阶段构建的例子:

# 第一阶段:构建环境
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# 第二阶段:运行环境
FROM alpine:3.18
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]

这个例子中,最终镜像只包含编译好的二进制文件和 ca-certificates,大小不到 20MB。而如果直接用 golang 镜像,至少 800MB。差距有多大?你想想看。

避坑指南:我曾经为了追求最小化,把基础镜像换成了 scratch(空镜像)。结果应用跑不起来,因为缺少动态链接库。后来我改用 distroless 镜像,它只包含运行应用所需的最小依赖,既安全又省心。

最后,我建议你定期审查镜像内容。可以用 docker history 查看每一层做了什么,也可以用 dive 工具可视化分析镜像。嗯,保持镜像「干净」,是安全的第一步。