基础镜像和 SBOM 验证

如果我们只想允许特定类型的基础镜像被部署, 可以在获取镜像信息后,将该信息保存到镜像 attestation 中。

漏洞扫描与验证 中,cosign-vuln 格式的 attestations 已经包含了基础镜像信息。 但这里我们将采用不同的方法,使用 syft 为镜像生成 SBOM。 SBOM 信息中也包含基础镜像信息。

在 ACP (Alauda Container Platform) 中,你可以在 Tekton Pipeline 里使用 trivysyft task 为镜像生成 SBOM。 这里我们使用 syft task 来生成 SBOM。

功能概述

该方法使用类似 syft 的工具为镜像生成 SBOM,然后使用 Kyverno 验证 SBOM:

  1. 使用 syft Tekton Task 为镜像生成 SBOM,并附加到镜像上。
  2. 配置 Kyverno 规则以验证 SBOM。
  3. 使用该镜像创建 Pod 来验证 SBOM。

使用场景

以下场景需要参考本文档中的指导:

  • 在 Kubernetes 集群中使用 Kyverno 实现基础镜像验证
  • 强制执行安全策略,仅允许特定基础镜像被部署
  • 在 CI/CD 流水线中设置自动化 SBOM 生成和验证
  • 确保生产环境中的基础镜像符合合规要求
  • 通过验证容器镜像的基础镜像信息,实现供应链安全控制

前提条件

  • 已安装 Tekton Pipelines、Tekton Chains 和 Kyverno 的 Kubernetes 集群
  • 已启用镜像推送功能的 registry
  • 已安装并配置好可访问集群的 kubectl CLI
  • 已安装 cosign CLI 工具
  • 已安装 jq CLI 工具

流程概览

步骤操作说明
1生成签名密钥使用 cosign 创建用于签署 artifact 的密钥对
2设置认证配置镜像推送所需的 registry 凭证
3配置 Tekton Chains设置 Chains 使用 OCI 存储并配置签名,禁用 TaskRun SLSA Provenance
4创建示例流水线创建包含 syft task 的流水线定义,用于生成 SBOM
5运行示例流水线使用正确的配置创建并运行 PipelineRun
6等待签名等待 Chains 为 PipelineRun 完成签名
7获取镜像信息从 PipelineRun 中提取镜像 URI 和 digest
8(可选)获取 SBOM attestation获取并验证 SBOM attestation
9使用 Kyverno 验证创建并应用 Kyverno 策略以验证基础镜像信息
10清理删除测试资源和策略

分步说明

步骤 1-3:基础设置

这些步骤与 快速开始:Signed Provenance 指南完全相同。请按照该指南完成以下内容:

步骤 4:创建示例流水线

这是一个 Pipeline 资源,用于构建镜像并生成 SBOM。

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: chains-demo-5
spec:
  params:
    - default: |-
        echo "Generate a Containerfile for building an image."

        cat << 'EOF' > Containerfile
        FROM ubuntu:latest
        ENV TIME=1
        EOF

        echo -e "\nContainerfile contents:"
        echo "-------------------"
        cat Containerfile
        echo "-------------------"
        echo -e "\nContainerfile generated successfully!"
      description: A script to generate a Containerfile for building an image.
      name: generate-containerfile
      type: string
    - default: <registry>/test/chains/demo-5:latest
      description: The target image address built
      name: image
      type: string
  results:
    - description: first image artifact output
      name: first_image_ARTIFACT_OUTPUTS
      type: object
      value:
        digest: $(tasks.build-image.results.IMAGE_DIGEST)
        uri: $(tasks.build-image.results.IMAGE_URL)
  tasks:
    - name: generate-containerfile
      params:
        - name: script
          value: $(params.generate-containerfile)
      taskRef:
        params:
          - name: kind
            value: task
          - name: catalog
            value: catalog
          - name: name
            value: run-script
          - name: version
            value: "0.1"
        resolver: hub
      timeout: 30m0s
      workspaces:
        - name: source
          workspace: source
    - name: build-image
      params:
        - name: IMAGES
          value:
            - $(params.image)
        - name: TLS_VERIFY
          value: "false"
      runAfter:
        - generate-containerfile
      taskRef:
        params:
          - name: kind
            value: task
          - name: catalog
            value: catalog
          - name: name
            value: buildah
          - name: version
            value: "0.9"
        resolver: hub
      timeout: 30m0s
      workspaces:
        - name: source
          workspace: source
        - name: registryconfig
          workspace: registryconfig
    - name: syft-sbom
      params:
        - name: COMMAND
          value: |-
            set -x

            mkdir -p .git

            echo "Generate sbom.json"
            syft scan $(tasks.build-image.results.IMAGE_URL)@$(tasks.build-image.results.IMAGE_DIGEST) -o cyclonedx-json=.git/sbom.json > /dev/null

            echo -e "\n\n"
            cat .git/sbom.json
            echo -e "\n\n"

            echo "Generate and Attestation sbom"
            syft attest $(tasks.build-image.results.IMAGE_URL)@$(tasks.build-image.results.IMAGE_DIGEST) -o cyclonedx-json
      runAfter:
        - build-image
      taskRef:
        params:
          - name: kind
            value: task
          - name: catalog
            value: catalog
          - name: name
            value: syft
          - name: version
            value: "0.1"
        resolver: hub
      timeout: 30m0s
      workspaces:
        - name: source
          workspace: source
        - name: registryconfig
          workspace: registryconfig
        - name: signkey
          workspace: signkey
  workspaces:
    - name: source
      description: The workspace for source code.
    - name: registryconfig
      description: The workspace for distribution registry configuration.
    - name: signkey
      description: The workspace for private keys and passwords used for image signatures.
TIP

本教程通过在流水线中内联生成 Containerfilegit-clone task 输出,演示了一个简化的工作流。 在生产环境中,你通常会:

  1. 使用 git-clone task 从你的仓库拉取源代码
  2. 使用源代码中已有的 Containerfile 构建镜像
  3. 这种方式可以确保正确的版本控制,并保持代码与流水线配置之间的分离
YAML 字段说明
  • 步骤 4:创建示例流水线 中的内容相同,但增加了以下内容:
    • workspaces
      • signkey:用于镜像签名的私钥和密码的 workspace。
    • tasks
      • syft-sbom:用于为镜像生成 SBOM 并上传 attestation 的 task。 :::

将其保存为名为 chains-demo-5.yaml 的 yaml 文件,并执行:

$ export NAMESPACE=<default>

# create the pipeline in the namespace
$ kubectl create -n $NAMESPACE -f chains-demo-5.yaml

pipeline.tekton.dev/chains-demo-5 created

步骤 5:运行示例流水线

这是一个 PipelineRun 资源,用于运行该流水线。

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  generateName: chains-demo-5-
spec:
  pipelineRef:
    name: chains-demo-5
  taskRunTemplate:
    serviceAccountName: <default>
  workspaces:
    - name: registryconfig
      secret:
        secretName: <registry-credentials>
    - name: source
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi
          storageClassName: <nfs>

:::details {title="YAML 字段说明"}

  • 步骤 5:运行示例流水线 中的内容大致一致,下面只介绍差异部分。
  • workspaces
    • signkey:签名密钥的 secret 名称。
      • secret.secretName:前一步 获取签名 secret 中准备好的签名 secret。但你需要在与 PipelineRun 相同的 namespace 中创建一个新的 secret。 :::

将其保存为名为 chains-demo-5.pipelinerun.yaml 的 yaml 文件,并执行:

$ export NAMESPACE=<default>

# create the pipeline run in the namespace
$ kubectl create -n $NAMESPACE -f chains-demo-5.pipelinerun.yaml

等待 PipelineRun 完成。

$ kubectl get pipelinerun -n $NAMESPACE -w

chains-demo-5-<xxxxx>     True        Succeeded   2m  2m

步骤 6:等待 PipelineRun 完成签名

等待 PipelineRun 具有 chains.tekton.dev/signed: "true" 注解。

$ export NAMESPACE=<default>
$ export PIPELINERUN_NAME=<chains-demo-5-xxxxx>

$ kubectl get pipelinerun -n $NAMESPACE $PIPELINERUN_NAME -o yaml | grep "chains.tekton.dev/signed"

    chains.tekton.dev/signed: "true"

一旦 PipelineRun 具有 chains.tekton.dev/signed: "true" 注解,就表示镜像已经完成签名。

步骤 7:从 PipelineRun 获取镜像

# Get the image URI
$ export IMAGE_URI=$(kubectl get pipelinerun -n $NAMESPACE $PIPELINERUN_NAME -o jsonpath='{.status.results[?(@.name=="first_image_ARTIFACT_OUTPUTS")].value.uri}')

# Get the image digest
$ export IMAGE_DIGEST=$(kubectl get pipelinerun -n $NAMESPACE $PIPELINERUN_NAME -o jsonpath='{.status.results[?(@.name=="first_image_ARTIFACT_OUTPUTS")].value.digest}')

# Combine the image URI and digest to form the full image reference
$ export IMAGE=$IMAGE_URI@$IMAGE_DIGEST

# Print the image reference
$ echo $IMAGE

<registry>/test/chains/demo-5:latest@sha256:a6c727554be7f9496e413a789663060cd2e62b3be083954188470a94b66239c7

该镜像将用于验证 SBOM。

步骤 8:(可选)获取 SBOM attestation

如果你对 SBOM attestation 的内容感兴趣,可以继续阅读下面的内容。

关于 cyclonedx SBOM attestation 的更多细节,请参考 cyclonedx SBOM attestation

请根据 获取签名公钥 一节获取签名公钥。

# Disable tlog upload and enable private infrastructure
$ export COSIGN_TLOG_UPLOAD=false
$ export COSIGN_PRIVATE_INFRASTRUCTURE=true

$ export IMAGE=<<registry>/test/chains/demo-5:latest@sha256:a6c727554be7f9496e413a789663060cd2e62b3be083954188470a94b66239c7>

$ cosign verify-attestation --key cosign.pub --type cyclonedx $IMAGE | jq -r '.payload | @base64d' | jq -s

输出结果会类似于下面这样,其中包含了镜像的组件信息。

:::details {title="cyclonedx SBOM attestation"}

{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://cyclonedx.org/bom",
  "predicate": {
    "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
    "bomFormat": "CycloneDX",
    "components": [
      {
        "bom-ref": "os:ubuntu@24.04",
        "licenses": [
          {
            "license": {
              "name": "GPL"
            }
          }
        ],
        "description": "Ubuntu 24.04.2 LTS",
        "name": "ubuntu",
        "type": "operating-system",
        "version": "24.04"
      }
    ],
    "metadata": {
      "timestamp": "2025-06-07T09:56:05Z",
      "tools": {
        "components": [
          {
            "author": "anchore",
            "name": "syft",
            "type": "application",
            "version": "1.23.1"
          }
        ]
      }
    }
  }
}

:::details {title="字段说明"}

  • predicateType:谓词类型。
  • predicate
    • components:镜像的组件。
      • bom-ref:组件的 BOM 引用。
      • licenses:组件的许可证。
        • license:组件的许可证。
          • name:许可证名称。
          • id:许可证 ID。
      • name:组件名称。
      • type:组件类型。
      • version:组件版本。
    • metadata:镜像的元数据。
      • timestamp:镜像时间戳。
      • tools
        • components:工具组件。
          • author:工具作者。
          • name:工具名称。
          • type:工具类型。
          • version:工具版本。 :::

步骤 9:验证基础镜像信息

步骤 9.1:创建 Kyverno 策略来验证基础镜像信息

TIP

此步骤需要集群管理员权限。

关于 Kyverno ClusterPolicy 的更多细节,请参考 Kyverno ClusterPolicy

策略如下:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-base-image
spec:
  webhookConfiguration:
    failurePolicy: Fail
    timeoutSeconds: 30
  background: false
  rules:
    - name: check-image
      match:
        any:
          - resources:
              kinds:
                - Pod
              namespaces:
                - policy
      verifyImages:
        - imageReferences:
            - "*"
            # - "<registry>/test/*"
          skipImageReferences:
            - "ghcr.io/trusted/*"
          failureAction: Enforce
          verifyDigest: false
          required: false
          useCache: false
          imageRegistryCredentials:
            allowInsecureRegistry: true
            secrets:
              # The credential needs to exist in the namespace where kyverno is deployed
              - registry-credentials

          attestations:
            - type: https://cyclonedx.org/bom
              attestors:
                - entries:
                    - attestor:
                      keys:
                        publicKeys: |- # <- The public key of the signer
                          -----BEGIN PUBLIC KEY-----
                          MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFZNGfYwn7+b4uSdEYLKjxWi3xtP3
                          UkR8hQvGrG25r0Ikoq0hI3/tr0m7ecvfM75TKh5jGAlLKSZUJpmCGaTToQ==
                          -----END PUBLIC KEY-----

                        ctlog:
                          ignoreSCT: true

                        rekor:
                          ignoreTlog: true

              conditions:
                - any:
                    - key: "{{ components[?type=='operating-system'] | [?name=='ubuntu' && (version=='22.04' || version=='24.04')] | length(@) }}"
                      operator: GreaterThan
                      value: 0
                      message: "The operating system must be Ubuntu 22.04 or 24.04, not {{ components[?type=='operating-system'].name[] }} {{ components[?type=='operating-system'].version[] }}"

                    - key: "{{ components[?type=='operating-system'] | [?name=='alpine' && (version=='3.18' || version=='3.20')] | length(@) }}"
                      operator: GreaterThan
                      value: 0
                      message: "The operating system must be Alpine 3.18 or 3.20, not {{ components[?type=='operating-system'].name[] }} {{ components[?type=='operating-system'].version[] }}"
                       PkgIDs: {{ scanner.result.Results[].Vulnerabilities[?CVSS.redhat.V3Score > `1.0`].PkgID[] }}.

:::details {title="YAML 字段说明"}

  • 该策略与 镜像签名验证 中的策略大体一致
  • spec.rules[0].verifyImages[].attestations[0].conditions
    • type:cyclonedx SBOM attestation 类型为 https://cyclonedx.org/bom
    • attestors:与上文相同。
    • conditions:需要验证的条件。
      • any:只要满足任一条件即可。
        • key: "{{ components[?type=='operating-system'] | [?name=='ubuntu' && (version=='22.04' || version=='24.04')] | length(@) }}":操作系统必须是 Ubuntu 22.04 或 24.04。
        • key: "{{ components[?type=='operating-system'] | [?name=='alpine' && (version=='3.18' || version=='3.20')] | length(@) }}":操作系统必须是 Alpine 3.18 或 3.20。 :::

将策略保存为名为 kyverno.verify-base-image.yaml 的 yaml 文件,并执行:

$ kubectl create -f kyverno.verify-base-image.yaml

clusterpolicy.kyverno.io/verify-base-image created

步骤 9.2:验证策略

在定义该策略的 policy namespace 中,创建一个 Pod 来验证该策略。

使用构建好的镜像创建 Pod。

$ export NAMESPACE=<policy>
$ export IMAGE=<<registry>/test/chains/demo-5:latest@sha256:a6c727554be7f9496e413a789663060cd2e62b3be083954188470a94b66239c7>

$ kubectl run -n $NAMESPACE base-image --image=${IMAGE} -- sleep 3600

如果你的基础镜像是 Ubuntu 22.04 或 24.04,Pod 将会创建成功。

ClusterPolicy 中的条件改为仅允许 Alpine 3.18 或 3.20。

conditions:
  - any:
      - key: "{{ components[?type=='operating-system'] | [?name=='alpine' && (version=='3.18' || version=='3.20')] | length(@) }}"
        operator: GreaterThan
        value: 0
        message: "The operating system must be Alpine 3.18 or 3.20, not {{ components[?type=='operating-system'].name[] }} {{ components[?type=='operating-system'].version[] }}"

然后创建一个 Pod 来验证该策略。

$ kubectl run -n $NAMESPACE deny-base-image --image=${IMAGE} -- sleep 3600

你会收到如下输出:

Error from server: admission webhook "mutate.kyverno.svc-fail" denied the request:

resource Pod/policy/deny-base-image was blocked due to the following policies

verify-base-image:
  check-image: 'image attestations verification failed, verifiedCount: 0, requiredCount:
    1, error: .attestations[0].attestors[0].entries[0].keys: attestation checks failed
    for <registry>/test/chains/demo-5:latest and predicate https://cyclonedx.org/bom:
    The operating system must be Alpine 3.18 or 3.20, not ["ubuntu"] ["24.04"]'

步骤 10:清理资源

删除前面步骤中创建的 Pod。

$ export NAMESPACE=<policy>
$ kubectl delete pod -n $NAMESPACE base-image

删除策略。

$ kubectl delete clusterpolicy verify-base-image

预期结果

完成本指南后:

  • 你已成功搭建了一个使用 Tekton Chains 生成 SBOM、并使用 Kyverno 进行基础镜像验证的工作环境
  • 你的容器镜像会自动在其 attestation 中包含 SBOM 信息
  • 在指定 namespace 中,只能部署使用可接受基础镜像构建的镜像
  • 具有不符合要求基础镜像的镜像会被 Kyverno 策略自动阻止
  • 你已经通过验证容器镜像的基础镜像信息,实现了一个基础的供应链安全控制

本指南为在 CI/CD 流水线中实施供应链安全提供了基础。在生产环境中,你应该:

  1. 配置正确的 namespace 隔离和访问控制
  2. 为签名密钥实施安全的密钥管理
  3. 为策略违规设置监控和告警
  4. 定期轮换签名密钥并更新安全策略

参考资料