基础镜像与 SBOM 验证

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

漏洞扫描与验证 中,cosign-vuln 格式的 attestation 已经包含了基础镜像信息。 但这里我们将采用另一种方法,使用 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创建示例 pipeline创建包含 syft task 的 pipeline 定义以生成 SBOM
5运行示例 pipeline使用正确的配置创建并运行 PipelineRun
6等待签名等待 Chains 对 PipelineRun 完成签名
7获取镜像信息从 PipelineRun 中提取镜像 URI 和 digest
8(可选)获取 SBOM attestation获取并验证 SBOM attestation
9使用 Kyverno 验证创建并应用 Kyverno policy 以验证基础镜像信息
10清理删除测试资源和 policy

分步说明

步骤 1-3:基础设置

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

步骤 4:创建示例 Pipeline

这是一个 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

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

  1. 使用 git-clone task 从你的仓库获取源代码
  2. 使用源代码中已有的 Containerfile 构建镜像
  3. 这种方式可以确保正确的版本控制,并保持代码与 pipeline 配置之间的分离
YAML 字段说明
  • 步骤 4:创建示例 Pipeline 中的内容基本一致,但新增了以下内容:
    • 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:运行示例 Pipeline

这是一个 PipelineRun 资源,用于运行该 pipeline。

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:运行示例 Pipeline 中的内容大体一致。下面仅介绍差异部分。
  • workspaces
    • signkey:签名密钥的 secret 名称。
      • secret.secretName:上一步 获取签名 secret 中准备的签名 secret。但你需要在与 pipeline run 相同的 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 的类型。
  • 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 policy 以验证基础镜像信息

TIP

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

关于 Kyverno ClusterPolicy 的更多信息,请参考 Kyverno ClusterPolicy

policy 如下:

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 字段说明"}

  • 该 policy 与 镜像签名验证 中的 policy 基本一致
  • 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。 :::

将该 policy 保存为名为 kyverno.verify-base-image.yaml 的 yaml 文件,并使用以下命令应用:

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

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

步骤 9.2:验证 policy

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

使用构建好的镜像创建一个 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 来验证该 policy。

$ 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

删除该 policy。

$ kubectl delete clusterpolicy verify-base-image

预期结果

完成本指南后:

  • 你已经完成了 Tekton Chains 用于 SBOM 生成以及 Kyverno 用于基础镜像验证的可用配置
  • 容器镜像会在其 attestation 中自动包含 SBOM 信息
  • 只有基础镜像符合要求的镜像才能在指定 namespace 中部署
  • 基础镜像不符合要求的镜像会被 Kyverno policy 自动阻止
  • 你已经通过验证容器镜像的基础镜像信息,实现了基础的供应链安全控制

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

  1. 配置适当的 namespace 隔离和访问控制
  2. 为签名密钥实施安全的密钥管理
  3. 建立针对 policy 违规的监控与告警
  4. 定期轮换签名密钥并更新安全 policy

参考资料