供应链攻击长什么样
传统的软件安全盯的是「自己的代码有没有洞」,而供应链安全盯的是「你依赖的每一层是否可信」。一个镜像的构成是:基础镜像(可能带系统依赖)→ 构建时安装的库和工具 → 应用代码 → 运行时配置。任何一个环节被污染,最终都会进到生产集群里执行。真实攻击里,最常见的是依赖包投毒(名字相似的恶意包被下载)、基础镜像被篡改或换源、以及镜像仓库被攻破后推送恶意镜像。
应对思路可以概括成四个动作:扫描(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,几秒钟就能回答「哪个环境在跑受影响的组件」。
仓库加固与 CI 落地
仓库是镜像的中转站,也是攻击者最喜欢的地方。加固要点:推镜像必须认证、镜像 tag 不可变(同 tag 禁止覆盖)、开放给公网的仓库只读、定期扫描仓库里所有镜像。自建仓库常用 Harbor,它自带漏洞扫描、复制策略和镜像不可变开关;云厂商的容器镜像服务也有类似能力。
CI 里把整条链串起来:构建 → 扫描(门禁)→ 签名 → 生成 SBOM → 推送 → 记录 digest。GitHub Actions 里这些步骤都有现成 action,重点是顺序和门禁参数别配错。
# GitHub Actions 镜像供应链步骤(示意)
jobs:
build:
runs-on: ubuntu-latest
permissions: {id-token: write, contents: read}
steps:
- uses: actions/checkout@v4
- run: docker build -t registry.example.com/app:${{ github.sha }} .
- run: trivy image --exit-code 1 --severity CRITICAL \
registry.example.com/app:${{ github.sha }}
- uses: sigstore/cosign-installer@v3
- run: cosign sign --yes registry.example.com/app:${{ github.sha }}
- run: syft registry.example.com/app:${{ github.sha }} \
-o cyclonedx-json > sbom.json
- run: docker push registry.example.com/app:${{ github.sha }}
落地节奏建议分三步走:先上扫描门禁(收益最大、改动最小),再上签名(配合准入才有效果),最后补齐 SBOM 归档。一步到位容易在团队里引起反弹,小步推进反而能真正持续下去。
运行时准入:只放行可信镜像
前四步保证「推到仓库的镜像可信」,但集群怎么知道拉到的镜像可信?靠 准入控制(Admission Control):Pod 创建时,准入控制器拦截请求,按策略校验镜像——必须验签通过、扫描无高危、来自白名单仓库,否则拒绝创建。工具用 Kyverno(策略即 Kubernetes 资源,好上手)或 OPA/Gatekeeper(Rego 策略,灵活)都行。
验签的准入策略要让集群运行时去调用 sigstore 验证签名,Kyverno 内置了 cosign 校验能力;仓库白名单用通配符匹配镜像地址。注意:准入校验只拦「创建」,拦不住「已经运行的镜像被替换」,所以还要配 imagePullPolicy 与 digest 引用,避免运行时悄悄换源。
# Kyverno 策略:只允许已签名镜像(示意)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-signature
spec:
validationFailureAction: Enforce
rules:
- name: check-signature
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences:
- "registry.example.com/*/*"
attestors:
- count: 1
entries:
- keyless:
subject: "https://github.com/example/app/.github/workflows/*"
issuer: "https://token.actions.githubusercontent.com"
准入策略上线要先 Audit 后 Enforce:先在审计模式下跑两周,看有多少存量镜像会被拦,评估影响面,再切换强制执行,避免一上线就把现有工作负载全部拦死。策略本身也要纳入 Git 管理,走审批变更。
一套镜像供应链安全清单
把全文收敛成一张可以照着执行的清单,从今天就能开始:
镜像供应链安全清单
1. 基础镜像固定 Dockerfile 里 pin 到 digest,禁止 latest
2. 多阶段构建 运行时镜像只带必要组件,缩小攻击面
3. CI 扫描门禁 CRITICAL 阻断发布,HIGH 限期修复留记录
4. 签名 keyless 签名,绑定构建身份(OIDC)
5. 用 digest 引用 tag 会被换,sha256 digest 不可变
6. SBOM 归档 构建时生成,随镜像存档、可检索
7. 仓库加固 推镜像必认证、tag 不可变、公开只读
8. 准入控制 先 Audit 再 Enforce,白名单 + 验签
9. 持续监控 定时扫仓库、跟进新 CVE、季度复盘
镜像供应链安全没有终点,它是一个持续运转的流程:扫描门禁会拦下新漏洞,签名与准入挡住伪造镜像,SBOM 让每一次漏洞通告都能秒级定位。把它和密钥管理、流水线审计放在同一个安全体系里看,DevOps 的「快」才有资格谈「稳」。安全左移之后,运维手里就不只是跑得快,而是知道自己在跑什么。