供应链攻击长什么样

传统的软件安全盯的是「自己的代码有没有洞」,而供应链安全盯的是「你依赖的每一层是否可信」。一个镜像的构成是:基础镜像(可能带系统依赖)→ 构建时安装的库和工具 → 应用代码 → 运行时配置。任何一个环节被污染,最终都会进到生产集群里执行。真实攻击里,最常见的是依赖包投毒(名字相似的恶意包被下载)、基础镜像被篡改或换源、以及镜像仓库被攻破后推送恶意镜像。

应对思路可以概括成四个动作:扫描(Scan)——把已知漏洞找出来;签名(Sign)——证明镜像来自可信构建流水线、没被篡改;清单(SBOM)——把镜像里的每一个组件记录下来,出事能回溯;准入(Admission)——让不符合策略的镜像根本进不了集群。这四步分别落在构建、推送、存储和运行四个环节。

# 一个镜像的信任链
Dockerfile ──构建──▶ 镜像 ──推送──▶ 仓库 ──拉取──▶ 集群运行
                │扫描         │签名/SBOM      │准入校验
                ▼             ▼              ▼
           漏洞门禁      可信来源证明    只放行合规镜像
本文核心知识点
  • 扫描Trivy 在构建与 CI 里卡住高危漏洞。
  • 签名Cosign 证明镜像来源并防篡改。
  • SBOM记录组件清单,漏洞回溯的关键。
  • 准入集群入口按签名与扫描结果放行。
容器镜像供应链安全配图:左侧方形仓库建筑经传送带把一排镜像层小方块送到放大镜与盾牌检查点、右侧方形机房、下方锁形与卡片图标的蓝色扁平科技插画
镜像从仓库到运行的每一站都该有检查点,而不只在上线那一下。

镜像扫描:用 Trivy 把漏洞找出来

扫描是供应链安全的第一道闸。开源工具里 Trivy 用得最广:它同时扫系统包漏洞(Debian/Ubuntu/Alpine 的 CVE)、语言依赖漏洞(pip/npm/go 等)、以及错误配置。本地一条命令就能扫,CI 里可以作为门禁,也可以接成定时扫仓库。

关键是把扫描做成门禁而不是报告:CI 里遇到高危(HIGH/CRITICAL)漏洞直接失败,等于从源头挡住,而不是上线后追着修。扫描结果要能和漏洞数据库同步更新,否则旧库会漏报新 CVE。

# 本地扫描镜像
trivy image nginx:1.25
# 只看高危及以上,输出表格
trivy image --severity HIGH,CRITICAL --format table nginx:1.25

# CI 门禁:发现 CRITICAL 即失败(退出码 1)
trivy image --exit-code 1 --severity CRITICAL \
  --ignore-unfixed registry.example.com/app:v1.2.3

# 扫仓库(持续监控,定时任务里跑)
trivy repo https://github.com/example/app

一个现实提醒:漏洞不是扫出来就能立刻消掉的。基础镜像的系统包漏洞往往依赖上游发布修复版本。务实的做法是分两级:CRITICAL 阻断发布,HIGH 限期处理并留豁免记录,而不是把所有漏洞都当成发布拦路虎,否则门禁很快会被团队绕过去。

镜像签名:用 Cosign 确认谁构建的

扫描解决「镜像里有什么漏洞」,签名解决「这个镜像是不是我们的人构建的、有没有被换过」。工具用 Cosign(sigstore 项目):构建流水线给镜像签名,任何要消费镜像的人或系统(仓库、集群准入)先验签,验不过就拒绝。

最推荐的是 keyless 签名:不需要自己管理密钥对,签名时用短期证书(身份绑定到 GitHub Actions 的 OIDC 身份),证书自动归档到公共日志,验证时任何人都能验。省去了私钥保管和轮换的麻烦,这也是供应链安全的加分项。

# keyless 签名(GitHub Actions 里自动带 OIDC 身份)
cosign sign --yes registry.example.com/app:v1.2.3
# 输出类似:Pushing signature to: registry.example.com/app@sha256:1f3a5c7e9b0d2f4a6c8e0b1d3f5a7c9e

# 验证签名与证书身份
cosign verify --certificate-identity-regexp '^https://github.com/example/app' \
  --certificate-oidc-issuer-regexp '^https://token.actions.githubusercontent.com' \
  registry.example.com/app:v1.2.3

签名要配合用 digest(sha256:…)而不是 tag 引用镜像:tag 可以被重新推送指向另一个镜像,digest 不可变。CI 里把「签名 + digest 记录」一起写进产物清单,运行端按 digest 拉取并验签,篡改路径就彻底堵上了。

SBOM:知道镜像里到底有什么

SBOM(软件物料清单)是镜像的「成分表」:列出里面每一个组件、版本和依赖关系。没有它,出了新 CVE 你都不知道自己有没有中招;有了它,一条「受影响组件:org.example/lib 1.2.3」就能直接检索出所有受影响镜像。

生成工具用 syft 或 Trivy 的 sbom 子命令,格式用行业标准 SPDX 或 CycloneDX。生成的 SBOM 要和镜像一起存进仓库(签名后归档),CI 里构建完立即生成,形成「镜像 + 签名 + SBOM」三件套。

# 生成 SBOM(CycloneDX 格式)
syft registry.example.com/app:v1.2.3 -o cyclonedx-json > sbom.cdx.json

# Trivy 也能生成
trivy image --format cyclonedx --output sbom.cdx.json registry.example.com/app:v1.2.3

# 新 CVE 发布后,用它反查受影响镜像
# (在 CI/定时任务中)grep 组件名 + 版本号匹配仓库里的 SBOM

SBOM 不是「生成了就完事」:要归档、要可检索、要随镜像走。仓库侧可以存到镜像的 attach 附件里(Harbor/registry 支持 artifact attach),审计或漏洞响应时按 digest 一键取回对应 SBOM,几秒钟就能回答「哪个环境在跑受影响的组件」。