分类: DevSecOps

  • 深入容器供应链安全:Trivy SCA 扫描 OOM 引发流水线假死与 Cosign Keyless 验签失败排查实战

    某次核心业务发布大面积卡死,根本原因是 Trivy 生成 SBOM 时对超过 2GB 的 Fat-JAR 进行深层解包触发 OOM-Kill,同时 CI/CD 中 Cosign Keyless 签名由于 OIDC Token 失效导致签名无效,引发 Kyverno 准入控制器验签超时拦截。本文直接给出针对超大镜像的 SCA 调优方案,并剖析基于 Fulcio/Rekor 的 Cosign 无密钥验签底层原理与拦截策略配置。

    现场还原:OOM 与 API Server 抖动

    排查过程中发现两处异常: 第一,GitLab CI 流水线在执行 SCA(软件成分分析)和 SBOM(软件物料清单)生成节点时大面积挂起,查看 Runner 所在节点的系统日志,满屏的 OOM-Kill

    $ dmesg -T | grep -i oom
    [Tue Oct  x xx:xx:xx xxxx] trivy invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
    [Tue Oct  x xx:xx:xx xxxx] Out of memory: Killed process 14582 (trivy) total-vm:4194304kB, anon-rss:2048576kB, file-rss:0kB, shmem-rss:0kB
    

    第二,部分侥幸通过 CI 的镜像在推送到生产 K8S 集群时,Pod 处于 CreateContainerConfigError,Event 提示 Kyverno 准入控制 Webhook 拦截:

    Error creating: admission webhook "check-image-signature.kyverno.svc" denied the request: 
    image index.docker.io/mycorp/payment-svc:v1.2.3 failed signature verification: 
    verify signature failed: getting transparency log entry: context deadline exceeded
    

    同时,K8S API Server 的 P99 延迟从平时 20ms 飙升到了 3000ms 以上。

    为什么 Trivy 在生成 SBOM 时会触发 OOM?

    在供应链安全体系中,SCA 扫描不仅要比对 OS 级别的漏洞(如 dpkg, rpm),更要解析应用依赖(如 Maven, npm)。涉事业务线打包了一个极其臃肿的 Java 镜像(超过 2.5GB),内部嵌套了大量的胖 JAR 包(Fat-JAR)。

    当使用 Trivy (版本 v0.49.1) 生成 CycloneDX 格式的 SBOM 时:

    trivy image --format cyclonedx --output sbom.json mycorp/payment-svc:v1.2.3
    

    底层原理是:Trivy 默认会分析镜像内所有的压缩文件(包括 .jar, .war, .tar.gz)。为了提取内部的 pom.xmlgo.mod 确认组件版本,Trivy 需要将这些归档文件加载到内存并解压到 /tmp 目录。 当遇到嵌套深度高、单体文件极大的 JAR 包时,Trivy 的 Goroutine 会并发解压,导致堆内存暴涨。若容器限制了 2GB RAM,必然被底层 Cgroup 对应的 OOM Killer 猎杀。

    解决与优化方案:

    1. 限制并发与文件类型:对大体积镜像屏蔽不必要的深层扫描,关闭并行解压。

    2. 挂载缓存与临时目录:将 Trivy 的临时解压目录映射到宿主机的高速 NVMe 磁盘上,而不是吃容器内存(tmpfs)。

    3. 调整命令参数

    # 增加临时目录环境变量,并跳过测试类或特定大型数据目录
    export TMPDIR=/mnt/host-disk/trivy-tmp
    trivy image \
      --format cyclonedx \
      --output sbom.json \
      --skip-dirs "/app/data" \
      --parallel 1 \
      --offline-scan \
      mycorp/payment-svc:v1.2.3
    

    Cosign Keyless 验签超时与 Kyverno 雪崩阻断

    解决了 CI 端的 SBOM 生成问题后,来看 K8S 端的拦截。 目前业界推崇 Sigstore 体系下的 Cosign Keyless(无密钥)签名。它不依赖静态私钥,而是依靠 OIDC 身份认证 -> Fulcio(颁发短期证书)-> Rekor(不可篡改的透明日志)这一闭环。

    在 CI 环境中(Cosign v2.2.3),签名的底层工作流是:

    cosign sign --yes index.docker.io/mycorp/payment-svc:v1.2.3
    

    Cosign 向 Rekor 提交签名记录。

    当 K8S 集群内的 Kyverno(v1.11.1)拦截到 Pod 创建请求时,它需要校验镜像签名。配置的 ClusterPolicy 如下:

    apiVersion: kyverno.io/v1
    kind: ClusterPolicy
    metadata:
      name: verify-image-signature
    spec:
      validationFailureAction: Enforce # 严格阻断
      webhookTimeoutSeconds: 5         # Webhook 超时 5 秒
      rules:
        - name: verify-signature
          match:
            any:
            - resources:
                kinds:
                  - Pod
          verifyImages:
          - imageReferences:
            - "index.docker.io/mycorp/*"
            attestors:
            - entries:
              - keyless:
                  subject: "https://gitlab.mycorp.com/*"
                  issuer: "https://gitlab.mycorp.com"
    

    超时雪崩的底层根因: 为了验证 Keyless 签名,Kyverno 必须向公网的 Rekor 服务器(rekor.sigstore.dev)发起出站 HTTP 请求,检索 transparency log。 由于生产环境所在的 VPC 进行了严格的公网出站限制(NAT 网关 ACL 变更),导致 Kyverno 请求 Rekor 的 TCP 建连一直卡在 SYN_SENT 状态,直到 5 秒超时。 由于设置了 validationFailureAction: Enforce 并且 K8S API Server 持续等待 Webhook 返回,大量发版请求同时卡住,直接导致 API Server 对应处理线程池耗尽,P99 延迟飙升。

    防御性重构: 基础设施的安全校验决不能成为系统可用性的单点瓶颈(SPOF)。

    1. 网络放行与私有化部署:在 NAT 网关显式放行 Sigstore 相关的域名(rekor.sigstore.dev, fulcio.sigstore.dev),长期方案是部署私有化的 Rekor/Fulcio 实例。

    2. Kyverno 容错配置:在未实现本地缓存时,将 failurePolicy 设为 IgnoreFail 是个需要权衡的问题。对于非金融核心链路,建议启用缓存并调整 Webhook 拦截策略:

    # 在 webhook 配置中启用缓存策略,并在极端网络断开时降级
    spec:
      failurePolicy: Ignore # 网络故障时不阻断 K8S 调度,改为告警
      webhookTimeoutSeconds: 3
      # Kyverno 1.11+ 支持使用 ttl 缓存验签结果,避免每次 Pod 扩容都请求公网
    

    常见问题

    Q1:Syft 和 Trivy 生成的 SBOM 格式 (SPDX/CycloneDX) 在后续消费时有何区别? SPDX 出身于 Linux 基金会,侧重于开源软件的许可证(License)合规性跟踪;CycloneDX 由 OWASP 驱动,原生地更侧重于漏洞管理(Vulnerability)和依赖路径分析。如果在 CI/CD 管道中重点是做 SCA 漏洞拦截并结合 Dependency-Track,建议统一输出为 CycloneDX 格式。

    Q2:Cosign 生成的 .sig 签名文件是如何与原镜像绑定的?删除原镜像标签会影响验签吗? Cosign 在 OCI 注册表(如 Harbor, Docker Hub)中并不直接修改原镜像,而是根据原镜像的 sha256 摘要创建一个附着对象(Attachment)。例如镜像 sha256 为 sha256:1234...,Cosign 会生成一个 tag 为 sha256-1234....sig 的新镜像层来存储签名内容。验签底层依赖的是 Digest 散列值,因此单纯删除或修改原镜像的 Tag,只要镜像文件的 Hash 未变,验签依然能够通过。

    Q3:遇到高度受限的离线环境 (Air-gapped) 怎么做 SCA 漏洞库更新和 Cosign 验签? 离线环境是供应链安全的痛点。针对 Trivy,需要在有网环境使用 trivy image --download-db-only 提取 trivy.dbtrivy-java.db,然后打包并通过内网推送到离线机器的 ~/.cache/trivy/ 目录;针对 Cosign 验签,必须放弃强依赖外网的 Keyless 方案,改用传统的基于 KMS 或本地静态公私钥对(cosign generate-key-pair)的签名模式,将公钥内置于 K8S 准入控制器中,实现完全内网闭环校验。