Build System Provenance Verification

In the SLSA provenance, there is a builder.id field, which is used to indicate the build environment of the image.

In this document, we will use this builder.id field to verify the image.

TIP

Since Tekton Chains has already handled both image signing and SLSA provenance generation in the preparation stage, we can directly reuse the process and images from Quick Start: Signed Provenance.

We will focus on verifying the SLSA provenance in this document.

TOC

Feature Overview

This method uses Chains to automatically generate SLSA Provenance for the built image and then use Kyverno to verify the provenance:

  1. Configure Tekton Chains to automatically generate SLSA Provenance for the built image.
  2. Use buildah Tekton Task to build the image.
  3. (Optional) Use cosign cli to verify the attestation.
  4. Configure Kyverno rules to verify the attestation.
  5. Use the image to create a Pod to verify the attestation.

Use Cases

The following scenarios require referring to the guidance in this document:

  • Verifying the build environment of container images using SLSA provenance
  • Implementing build system provenance verification using CUE or Rego policies
  • Enforcing security policies to only allow images built in specific build environments
  • Setting up automated build system provenance verification in CI/CD pipelines
  • Ensuring build system provenance integrity and authenticity in production environments

Prerequisites

  • A Kubernetes cluster with Tekton Pipelines, Tekton Chains and Kyverno installed
  • A registry with image pushing enabled
  • kubectl CLI installed and configured to access your cluster
  • cosign CLI tool installed
  • jq CLI tool installed

Process Overview

StepOperationDescription
1Generate signing keysCreate a key pair for signing artifacts using cosign
2Set up authenticationConfigure registry credentials for image pushing
3Configure Tekton ChainsSet up Chains to use OCI storage and configure signing
4Create a sample pipelineCreate a pipeline definition with necessary tasks and workspaces
5Run a sample pipelineCreate and run a PipelineRun with proper configuration
6Wait for signingWait for the PipelineRun to be signed by Chains
7Get image informationExtract image URI and digest from the PipelineRun
8(Optional) Verify signatures with cosignVerify the image attestation using cosign CLI
9Verify signatures with KyvernoConfigure and verify the image attestation using Kyverno policies
10Clean up resourcesDelete the test Pods and policies

Step-by-Step Instructions

Steps 1-7: (Optional) Basic Setup

NOTE

If you change the builder.id field, you need to re-run the pipeline to generate the image. Because the old image is not signed with the new builder.id, so it will be blocked by the policy.

Otherwise, you can skip this step, use the old image to verify the policy.

These steps are identical to the Quick Start: Signed Provenance guide. Please follow the instructions in that guide for:

Step 8: (Optional) Verify the builder info with cosign

TIP

This step is optional and should be performed when you need to verify the authenticity of the image builder by cosign.

If you interested how to use CUE or Rego to verify the builder info, you can continue to read the following content.

Get the signing public key according to the Get the signing public key section.

Cosign provides two ways to validate the attestation:

The following will show the verification methods of these two ways.

Way 1: Use CUE to verify

Generate the CUE file to verify the builder info.

// The predicate must match the following constraints.
predicate: {
    builder: {
        id: "https://alauda.io/builders/tekton/v1"
    }
}

Save the CUE file to builder.cue

Verify the builder info with cosign.

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

$ export IMAGE=<<registry>/test/chains/demo-1:latest@sha256:93635f39cb31de5c6988cdf1f10435c41b3fb85570c930d51d41bbadc1a90046>

$ cosign verify-attestation --key cosign.pub --type slsaprovenance --policy builder.cue $IMAGE

Receive the output like this, means the builder info verification is successful.

will be validating against CUE policies: [builder.cue]
will be validating against CUE policies: [builder.cue]

Verification for <registry>/test/chains/demo-1:latest@sha256:8ac1af8dd89652bf32abbbd0c5f667ae9fe6d92c91972617e70b5398303c8e27 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
{"payloadType":"application/vnd.in-toto+json","payload":"","signatures":[]}

Change the builder id in the builder.cue file to an other value https://alauda.io/builders/tekton/v2, and verify again.

$ cosign verify-attestation --key cosign.pub --type slsaprovenance --policy builder.cue $IMAGE

Receive the output like this, means the builder info verification is failed.

will be validating against CUE policies: [builder.cue]
will be validating against CUE policies: [builder.cue]
There are 2 number of errors occurred during the validation:

- predicate.builder.id: conflicting values "https://alauda.io/builders/tekton/v1" and "https://alauda.io/builders/tekton/v2"
- predicate.builder.id: conflicting values "https://alauda.io/builders/tekton/v1" and "https://alauda.io/builders/tekton/v2"
Error: 2 validation errors occurred
error during command execution: 2 validation errors occurred

Way 2: Use Rego to verify

Generate the Rego file to verify the builder info.

package signature

default allow = false

# Define the allowed builder.id
allowed_builder_id = "https://alauda.io/builders/tekton/v1"

# Verify the builder.id
allow {
    # Check if the builder.id in the predicate is equal to the allowed value
    input.predicate.builder.id == allowed_builder_id
}

# Return error message when not match
deny[msg] {
    input.predicate.builder.id != allowed_builder_id
    msg := sprintf("unexpected builder.id: %v, expected: %v", [input.predicate.builder.id, allowed_builder_id])
}

Save the Rego file to builder.rego

Verify the builder info with cosign.

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

$ export IMAGE=<<registry>/test/chains/demo-1:latest@sha256:93635f39cb31de5c6988cdf1f10435c41b3fb85570c930d51d41bbadc1a90046>

$ cosign verify-attestation --key cosign.pub --type slsaprovenance --policy builder.rego $IMAGE

Receive the output like this, means the builder info verification is successful.

will be validating against Rego policies: [builder.rego]
will be validating against Rego policies: [builder.rego]

Verification for <registry>/test/chains/demo-1:latest --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
{"payloadType":"application/vnd.in-toto+json","payload":"","signatures":[]}

Change the builder id in the builder.rego file to an other value https://alauda.io/builders/tekton/v2, and verify again.

$ cosign verify-attestation --key cosign.pub --type slsaprovenance --policy builder.rego $IMAGE

Receive the output like this, means the builder info verification is failed.

will be validating against Rego policies: [builder.rego]
will be validating against Rego policies: [builder.rego]
There are 2 number of errors occurred during the validation:

- expression value, false, is not true
- expression value, false, is not true
Error: 2 validation errors occurred
error during command execution: 2 validation errors occurred

Step 9: Verify the signature with Kyverno

TIP

This step requires cluster administrator privileges.

The content of the provenance is roughly as follows, we will use the builder.id field to verify the build environment.

{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://slsa.dev/provenance/v0.2",
  "predicate": {
    "buildType": "tekton.dev/v1beta1/TaskRun",
    "builder": {
      "id": "https://alauda.io/builders/tekton/v1"
    },
    "materials": [
      {
        "digest": {
          "sha256": "8d5ea9ecd9b531e798fecd87ca3b64ee1c95e4f2621d09e893c58ed593bfd4c4"
        },
        "uri": "oci://<registry>/devops/tektoncd/hub/buildah"
      }
    ],
    "metadata": {
      "buildFinishedOn": "2025-06-06T10:21:27Z",
      "buildStartedOn": "2025-06-06T10:20:55Z"
    }
  }
}

Step 9.1: Create a Kyverno policy to allow only images built in specific build environments to be deployed

TIP

More details about Kyverno ClusterPolicy, please refer to Kyverno ClusterPolicy

The policy is as follows:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-tekton-built-images
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://slsa.dev/provenance/v0.2
              attestors:
                - entries:
                    - 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:
                - all:
                    - key: "{{ builder.id }}"
                      operator: Equals
                      value: "https://alauda.io/builders/tekton/v1"
                      message: "The builder.id must be equal to https://alauda.io/builders/tekton/v1, not {{ builder.id }}"
Explanation of YAML fields
  • The policy is largely consistent with the one in Image Signature Verification. Below only introduces the differences.
  • spec.rules[0].verifyImages[].attestations[0].conditions
    • type: The slsa provenance type is https://slsa.dev/provenance/v0.2 or https://slsa.dev/provenance/v1.
    • attestors: the same as above.
    • conditions: The conditions to be verified.
      • all: All conditions must be met.
        • key: "{{ builder.id }}": This check the builder.id field in the attestation is equal to https://alauda.io/builders/tekton/v1

Need to adjust the configuration

  • spec.rules[].attestors[].entries[].keys.publicKeys: The public key of the signer.
    • This public key is the same as the public key cosign.pub in the signing-secrets secret.
    • The public key can be obtained from the Get the Signing Public Key section.

Save the policy to a yaml file named kyverno.verify-tekton-built-images.yaml and apply it with:

$ kubectl apply -f kyverno.verify-tekton-built-images.yaml

clusterpolicy.kyverno.io/verify-tekton-built-images configured

Step 9.2: Verify the policy

In the policy namespace where the policy is defined, create a Pod to verify the policy.

Use the built image to create a Pod.

$ export NAMESPACE=<policy>
$ export IMAGE=<<registry>/test/chains/demo-1:latest@sha256:93635f39cb31de5c6988cdf1f10435c41b3fb85570c930d51d41bbadc1a90046>

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

pod/built created

The Pod will be created successfully.

$ kubectl get pod -n $NAMESPACE built

NAME      READY   STATUS    RESTARTS   AGE
built   1/1     Running   0          10s

Change the builder id in the ClusterPolicy to an other value https://alauda.io/builders/tekton/v2, and verify again.

conditions:
  - all:
      - key: "{{ builder.id }}"
        operator: Equals
        value: "https://alauda.io/builders/tekton/v2"
        message: "The builder.id must be equal to https://alauda.io/builders/tekton/v2, not {{ builder.id }}"
$ kubectl run -n $NAMESPACE unbuilt --image=${IMAGE} -- sleep 3600

Receive the output like this, means the Pod is blocked by the policy.

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

resource Pod/policy/unbuilt was blocked due to the following policies

verify-tekton-built-images:
  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-1@sha256:93635f39cb31de5c6988cdf1f10435c41b3fb85570c930d51d41bbadc1a90046
    and predicate https://slsa.dev/provenance/v0.2: The builder.id must be equal to
    https://alauda.io/builders/tekton/v2, not https://alauda.io/builders/tekton/v1'

Step 10: Clean up the resources

Delete the Pods created in the previous steps.

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

Delete the policy.

$ kubectl delete clusterpolicy verify-tekton-built-images

Expected Results

After completing this guide:

  • You have a working setup with Tekton Chains for generating SLSA provenance
  • Your container images are automatically signed with build system provenance during the build process
  • You can verify the build environment of images using either CUE or Rego policies
  • Only images built in the specified build environment can be deployed in the specified namespace
  • Images built in unauthorized build environments are automatically blocked by Kyverno policies
  • You have implemented a basic build system provenance verification control for your container images

This guide provides a foundation for implementing build system provenance verification in your CI/CD pipelines. In a production environment, you should:

  1. Configure proper namespace isolation and access controls
  2. Implement secure key management for signing keys
  3. Set up monitoring and alerting for policy violations
  4. Regularly rotate signing keys and update security policies
  5. Consider implementing additional security controls like vulnerability scanning

References