标签: Kyverno

  • 深入 K8S 供应链安全陷阱排查:大体积镜像 SBOM 解析 OOM 与 Cosign 验签超时引发的 Admission Webhook 阻塞实战

    核心结论:在 CI/CD 中直接对巨型单体镜像执行全层级 SBOM 扫描极易引发 Syft 进程 OOM。同时,K8S 集群中集成 Cosign 准入控制时,若未开启本地缓存,验签阶段高频的公网 OCI 注册表请求延迟会导致 Admission Webhook 超时(默认3秒),最终引发集群 Pod 创建大面积失败。需通过调整扫描作用域(Scope)与开启准入控制器验签缓存来彻底根治。

    某次排查过程中,我们落地完整的供应链安全体系(SBOM 生成 -> 镜像签名 -> 准入控制 -> SCA 漏洞分析)。架构上线后,不仅 CI 构建节点频繁告警内存耗尽,生产 K8S 集群甚至出现了常规业务 Pod 扩容大面积处于 ContainerCreating 或直接被 API Server 拒绝的情况。

    这绝不是简单的工具堆砌问题,而是深层次的资源隔离与 OCI 协议交互延迟导致的系统级瓶颈。

    陷阱一:单体镜像 SBOM 生成引发的 CI 节点 OOM

    我们在 GitLab Runner(4C8G 配置)中使用 Syft (v1.3.0) 对业务镜像生成 SBOM。对于基础的 Go/Python 服务毫无波澜,但当扫描到一个 2.5GB 包含大量嵌合 JAR 包的 Java 遗留单体应用时,Pipeline 直接以 Exit Code 137 崩溃。

    查看系统日志直击现场:

    dmesg -T | grep -i oom
    # [Mon Oct 16 10:23:45 2023] syft invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
    # [Mon Oct 16 10:23:45 2023] Memory cgroup out of memory: Killed process 14592 (syft) total-vm:9854320kB, anon-rss:8388608kB, file-rss:0kB, shmem-rss:0kB
    

    Syft 吃掉了整整 8GB 内存。底层原因在于默认配置下,Syft 的扫描机制极其激进:

    1. 全层级解包 (All-Layers):默认行为下,Syft 会遍历镜像的每一层(Layer)。如果业务在 Dockerfile 中有大量 RUN 和 COPY 操作,Syft 会在内存中重建整个文件系统的历史状态。

    2. 深度递归 Cataloger:针对 java-archive-cataloger,它会递归解压嵌套的 .jar、.war、.ear 文件(Fat JAR 往往嵌套多层),导致内存中维护的包依赖抽象语法树(AST)呈指数级膨胀。

    防雷与修复配置: 放弃毫无意义的全层级历史扫描,改为仅扫描最终态(Squashed),并限制部分高开销但不必要的解析器。

    # 优化前的致命命令
    syft packages docker:registry.local/my-app:v1.2.3 -o cyclonedx-json > sbom.json
    
    # 优化后的防御性配置命令
    syft packages docker:registry.local/my-app:v1.2.3 \
      --scope squashed \
      --catalogers -javascript-lockfile,-python-package-cataloger \
      -o cyclonedx-json > sbom.json
    

    注:--scope squashed 仅评估镜像的最终可见层,这使得内存消耗直接从 8GB 暴降至 600MB 左右。

    为什么 Cosign 验签会导致 K8S Admission Webhook 级联超时?

    这是整个防御体系中最容易翻车的一环。我们在 K8S 中部署了 Kyverno (v1.11.4) 作为 Admission Controller,配合 Cosign (v2.2.3) 实现拦截未签名镜像。

    上线后,发现高并发发布时,Pod 经常卡在调度阶段,API Server 的日志中狂刷:

    E1016 14:05:22.123456 1 dispatcher.go:180] failed calling webhook "mutate.kyverno.svc-fail": failed to call webhook: Post "https://kyverno-svc.kyverno.system.svc:443/mutate?timeout=3s": context deadline exceeded
    

    底层原理剖析: 当你使用 cosign sign 对镜像签名后,Cosign 并不会修改原有镜像的内容(这也保证了 digest 不变),而是将签名数据作为一个特殊的 OCI Artifact 推送到同一个 Registry。签名 Tag 的命名规范是 sha256-.sig。

    当 K8S API Server 收到创建 Pod 的请求时:

    1. API Server 暂停创建,调用 Kyverno Webhook。

    2. Kyverno 提取 Pod 镜像地址,利用内部集成的 Cosign 库,向镜像仓库(如 Harbor)发起网络请求。

    3. 查询 .sig,并拉取签名公钥进行密码学校验。

    4. K8S 默认 Webhook 超时时间只有 3秒!

    如果此时发生以下任一情况:Registry 负载突增导致响应慢、DNS 解析抖动、或者 Harbor 的 Rate Limit 触发,请求耗时超过 3 秒,API Server 就会无情切断连接(context deadline exceeded),并在默认 FailurePolicy=Fail 的配置下,直接拒绝创建 Pod。这就是所谓“安全插件阻塞了主干道”。

    高可用落地改造:

    1. 开启本地缓存与控制并发 不要让 Webhook 每次都去请求 Registry。在 Kyverno 中必须显式启用验签缓存,并调大 K8S 原生的 Webhook Timeout。

    修改 Kyverno 部署参数与策略配置:

    # K8S MutatingWebhookConfiguration/ValidatingWebhookConfiguration 
    # 建议将 timeoutSeconds 从默认的 3 调整为 10 或 15,给网络抖动留出余地
    timeoutSeconds: 10
    
    # Kyverno ClusterPolicy 配置片段
    apiVersion: kyverno.io/v1
    kind: ClusterPolicy
    metadata:
      name: check-image-signature
    spec:
      webhookTimeoutSeconds: 10
      rules:
        - name: check-signature
          match:
            resources:
              kinds:
                - Pod
          verifyImages:
            - imageReferences:
                - "registry.local/my-biz/*"
              mutateDigest: true
              verifyDigest: true
              # 核心参数:开启验签结果缓存,基于 Image Digest,缓存命中极大降低 Registry 压力
              cacheEnabled: true
              key: |-
                -----BEGIN PUBLIC KEY-----
                MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElxyz...
                -----END PUBLIC KEY-----
    

    2. 镜像拉取与签名校验在同一个网络域 尽量确保 K8S 节点所在的网络到 Registry 之间没有经过多层低性能的 NAT 或防火墙,避免验签过程在 TCP 握手和 TLS 协商上浪费掉宝贵的几百毫秒。

    软件成分分析 (SCA):Trivy 误报阻断流水线的防御性实践

    SBOM 生成完后,不可避免要将其喂给 Trivy (v0.49.1) 这类 SCA 工具进行 CVE 扫描。如果你在 CI/CD 中设置了 trivy sbom ./sbom.json --exit-code 1 --severity CRITICAL,那么你的流水线大概率会立刻瘫痪。

    在实战中,SCA 扫描器常对间接依赖给出大量极高危 CVE,但实际上代码执行路径根本触碰不到漏洞函数。作为运维与安全架构师,我们绝不能生硬地充当“阻塞者”,而需要提供一套优雅的豁免(VEX)与基线管理机制。

    Trivy 防御性配置最佳实践: 利用 .trivyignore 文件进行 CVE 基线化管理,结合 ignore-unfixed 参数(过滤尚未有官方修复方案的漏洞,逼迫研发修复是无效操作)。

    trivy sbom ./sbom.json \
      --ignore-unfixed \
      --ignorefile ./.trivyignore \
      --severity HIGH,CRITICAL \
      --exit-code 1 \
      --format table
    

    .trivyignore 文件应当存放在业务代码库的根目录,实施“谁豁免,谁负责,需审计”的原则,上面注明豁免的 CVE ID 及原因注释。

    常见问题 (FAQ)

    Q1: 使用 Cosign 对容器镜像签名后,镜像原本的 sha256 Digest 会发生变化吗?会导致现有的 GitOps (ArgoCD) 同步失败吗? 不会变。Cosign 采用的是 Detached Signature(分离签名)机制。签名本身作为独立的 OCI 对象存在 Registry 中。它并没有修改原镜像的 Manifest 数据结构,所以原镜像的 Digest 和计算出的 Hash 完全一致,对 GitOps 工具层绝对透明。

    Q2: 业务基础镜像(如 alpine 或 debian)经常被 SCA 扫出几十个系统级 CVE,研发表示无能为力,如何处理这种僵局? 不要试图让业务研发去修复 OS 层的包漏洞。正确做法是:基础设施团队提供经过安全加固和漏洞清理的统一 Base Image。CI/CD 流水线中,SCA 扫描应该区分 OS 包和应用语言包(通过 --vuln-type library 仅扫描代码级依赖),系统级的 CVE 应由基础架构侧在根镜像环节收口闭环。

    Q3: SPDX 和 CycloneDX 两种 SBOM 格式,在企业落地时到底选哪个? 实战建议选 CycloneDX。虽然 SPDX 背后有 Linux 基金会背书,但在云原生与应用安全领域(尤其是和漏洞管理、VEX 漏洞可利用性交换结合时),OWASP 主导的 CycloneDX 生态工具链更为完善。其 JSON 结构的易读性以及对现代 CI/CD 解析工具(如 Dependency-Track)的原生支持度显著高于 SPDX。

  • 深入容器供应链安全: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.xml 或 go.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 设为 Ignore 或 Fail 是个需要权衡的问题。对于非金融核心链路,建议启用缓存并调整 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.db 和 trivy-java.db,然后打包并通过内网推送到离线机器的 ~/.cache/trivy/ 目录;针对 Cosign 验签,必须放弃强依赖外网的 Keyless 方案,改用传统的基于 KMS 或本地静态公私钥对(cosign generate-key-pair)的签名模式,将公钥内置于 K8S 准入控制器中,实现完全内网闭环校验。