Source Code Repository Verification

In Tekton Chains, it can collect specific inputs and outputs from the PipelineRun and record them in the SLSA Provenance.

TIP

See Tekton Chains Type Hinting in above section for more details.

We can use this feature to include the code repository information in the SLSA Provenance information. Then we can verify the code repository in kyverno.

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 git Tekton Task to get the source code repository.
  3. Use buildah Tekton Task to build the image.
  4. Declare the git and buildah results information in the results of the Pipeline. This facilitates recording the source code repository and commit information for the image.
  5. Configure Kyverno rules to verify the source code repository.
  6. Use the image to create a Pod to verify the source code repository.

Use Cases

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

  • Implementing source code repository verification in Kubernetes clusters using Kyverno
  • Enforcing security policies to only allow images built from specific source code repositories to be deployed
  • Setting up automated source code repository verification in CI/CD pipelines
  • Ensuring image provenance and source code authenticity in production environments
  • Implementing supply chain security controls for container images by verifying their source code origins

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, disable TaskRun SLSA Provenance
4Create a sample pipelineCreate a pipeline definition with git clone task and buildah task
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) Get SLSA ProvenanceGet and verify the SLSA Provenance attestation
9Verify with KyvernoCreate and apply Kyverno policy to verify image source repository
10Clean upDelete test resources and policies

Step-by-Step Instructions

Steps 1-3: Basic Setup

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

  • Step 1: Generate Signing Keys

  • Step 2: Set up Authentication

  • Step 3: Configure Tekton Chains

    To avoid Tekton Chains generating SLSA Provenance for both TaskRun and PipelineRun, which will affect the verification of kyverno later, we first disable the SLSA Provenance for TaskRun.

    TIP

    This process requires platform administrator privileges to configure.

    $ kubectl patch tektonconfigs.operator.tekton.dev config --type=merge -p='{
      "spec": {
        "chain": {
          "artifacts.taskrun.storage": ""
        }
      }
    }'

Step 4: Create a Sample Pipeline

In the previous image build pipeline, add a git clone task, and save the output of the git task to the results of the PipelineRun.

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: chains-demo-3
spec:
  params:
    - default: |-
        echo "Simulate cloning the code and write the repository URL and commit message into the results."

        # This commit sha must be a valid commit sha [0-9a-f]{40}.
        cat << 'EOF' > $(results.array-result.path)
        [
          "https://github.com/tektoncd/pipeline",
          "cccccaaaa0000000000000000000000000000000"
        ]
        EOF

        echo -e "\nResults:"
        echo "-------------------"
        cat $(results.array-result.path)
        echo "-------------------"
        echo -e "\nClone successfully!"
      description: A script to simulate cloning the code and write the repository URL and commit message into the results.
      name: generate-git-clone-results
      type: string
    - default: |-
        echo "Generate a Dockerfile for building an image."

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

        echo -e "\nDockerfile contents:"
        echo "-------------------"
        cat Dockerfile
        echo "-------------------"
        echo -e "\nDockerfile generated successfully!"
      description: A script to generate a Dockerfile for building an image.
      name: generate-dockerfile
      type: string
    - default: <registry>/test/chains/demo-3: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)
    - description: first repo artifact input
      name: source_repo_ARTIFACT_INPUTS
      type: object
      value:
        digest: sha1:$(tasks.git-clone.results.array-result[1])
        uri: $(tasks.git-clone.results.array-result[0])
  tasks:
    - name: git-clone
      params:
        - name: script
          value: $(params.generate-git-clone-results)
      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: generate-dockerfile
      params:
        - name: script
          value: $(params.generate-dockerfile)
      runAfter:
        - git-clone
      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-dockerfile
      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: dockerconfig
          workspace: dockerconfig
  workspaces:
    - name: source
      description: The workspace for source code.
    - name: dockerconfig
      description: The workspace for Docker configuration.
TIP

This tutorial demonstrates a simplified workflow by generating the Dockerfile and git-clone task output inline within the pipeline. In production environments, you would typically:

  1. Use the git-clone task to fetch source code from your repository
  2. Build the image using the Dockerfile that exists in your source code
  3. This approach ensures proper version control and maintains the separation between code and pipeline configuration
Explanation of YAML fields
  • Most fields are the same as in Step 4: Create a Sample Pipeline. Below only introduces the differences.
  • params
    • generate-git-clone-results: A script to simulate cloning the code and write the repository URL and commit message into the results.
  • results
    • source_repo_ARTIFACT_INPUTS: The source code repository URL and commit message.
      • digest: The commit sha of the source code repository.
    • This format is compliant with Tekton Chains, see Tekton Chains Type Hinting in above section for more details.

Need to adjust the configuration

  • params:
    • generate-dockerfile
      • default: Adjust the from image address.
    • image:
      • default: The target image address built.

Save into a yaml file named chains.demo-3.pipeline.yaml and apply it with:

$ export NAMESPACE=<default>
$ kubectl apply -n $NAMESPACE -f chains.demo-3.pipeline.yaml

Step 5: Run a Sample Pipeline

This is a PipelineRun resource, which is used to run the pipeline.

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

Save into a yaml file named chains.demo-3.pipelinerun.yaml and apply it with:

$ export NAMESPACE=<default>

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

Wait for the PipelineRun to complete.

$ kubectl get pipelinerun -n $NAMESPACE -w

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

Step 6: Wait for the PipelineRun to be signed

Wait for the PipelineRun has chains.tekton.dev/signed: "true" annotation.

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

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

    chains.tekton.dev/signed: "true"

Once the PipelineRun has chains.tekton.dev/signed: "true" annotation, means the image is signed.

Step 7: Get the image from the 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-3:latest@sha256:93635f39cb31de5c6988cdf1f10435c41b3fb85570c930d51d41bbadc1a90046

This image will be used to verify the source code repository.

Step 8: (Optional) Get the SLSA Provenance attestation

TIP

If you interested about the SLSA Provenance attestation content, you can continue to read the following content.

More details about the SLSA Provenance attestation, please refer to SLSA Provenance

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

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

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

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

The output will be similar to the following, which contains the SLSA Provenance attestation.

SLSA Provenance attestation
{
  "_type": "https://in-toto.io/Statement/v0.1",
  "subject": [
    {
      "name": "<registry>/test/chains/demo-3:latest",
      "digest": {
        "sha256": "db2607375049e8defa75a8317a53fd71fd3b448aec3c507de7179ded0d4b0f20"
      }
    }
  ],
  "predicateType": "https://slsa.dev/provenance/v0.2",
  "predicate": {
    "buildConfig": {
      "tasks": null
    },
    "buildType": "tekton.dev/v1beta1/PipelineRun",
    "builder": {
      "id": "https://alauda.io/builders/tekton/v1"
    },
    "invocation": {
      "parameters": {
        "image": "<registry>/test/chains/demo-3:latest"
      }
    },
    "materials": [
      {
        "digest": {
          "sha256": "bad5d84ded24307d12cacc9ef37fc38bce90ea5d00501f43b27d0c926be26f19"
        },
        "uri": "oci://<registry>/devops/tektoncd/hub/run-script"
      },
      {
        "digest": {
          "sha1": "cccccaaaa0000000000000000000000000000000"
        },
        "uri": "https://github.com/tektoncd/pipeline"
      }
    ],
    "metadata": {
      "buildFinishedOn": "2025-06-06T10:28:21Z",
      "buildStartedOn": "2025-06-06T10:27:34Z"
    }
  }
}
Description of the fields
  • predicateType: The type of the predicate.
  • predicate:
    • buildConfig:
      • tasks: The tasks of the build.
    • buildType: The type of the build, this is tekton.dev/v1beta1/PipelineRun.
    • builder:
      • id: The id of the builder, this is https://alauda.io/builders/tekton/v1.
    • invocation:
      • parameters: The parameters of the build.
    • materials: The materials of the build.
      • uri:
        • oci://<registry>/devops/tektoncd/hub/run-script: The image of the task used.
        • https://github.com/tektoncd/pipeline: The source code repository of the task.
    • metadata: The metadata of the build.
      • buildFinishedOn: The time when the build finished.
      • buildStartedOn: The time when the build started.

Step 9: Verify image source repository restriction with Kyverno

Step 9.1: Create a Kyverno policy to allow only images built from specific source code repositories to be deployed

TIP

This step requires cluster administrator privileges.

More details about Kyverno ClusterPolicy, please refer to Kyverno ClusterPolicy

The policy is as follows:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-code-repository-material
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: "{{ buildType }}"
                      operator: Equals
                      value: "tekton.dev/v1beta1/PipelineRun"
                      message: "The buildType must be equal to tekton.dev/v1beta1/PipelineRun, not {{ buildType }}"

                    - key: "{{ materials[?starts_with(uri, 'https://github.com/tektoncd/')] | length(@) }}"
                      operator: GreaterThan
                      value: 0
                      message: "The materials must have at least one entry starts with https://github.com/tektoncd/, {{ materials }}"
Explanation of YAML fields
  • The policy is largely consistent with the one in Image Signature Verification
  • spec.rules[].verifyImages[].attestations[].conditions: The conditions to verify.
    • all: The all conditions must be met.
      • key: "{{ buildType }}": The build type must be equal to tekton.dev/v1beta1/PipelineRun.
      • key: "{{ materials[?starts_with(uri, 'https://github.com/tektoncd/')] | length(@) }}": The materials must have at least one entry starts with https://github.com/tektoncd/.

Save into a yaml file named verify-code-repository-material.yaml and apply it with:

$ kubectl create -f verify-code-repository-material.yaml

clusterpolicy.kyverno.io/verify-code-repository-material created

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-3:latest@sha256:db2607375049e8defa75a8317a53fd71fd3b448aec3c507de7179ded0d4b0f20>

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

pod/built-from-specific-repo created

The Pod will be created successfully.

$ kubectl get pod -n $NAMESPACE built-from-specific-repo

NAME                      READY   STATUS    RESTARTS   AGE
built-from-specific-repo   1/1     Running   0          10s

Change the code repository in the ClusterPolicy to an other value https://gitlab.com/, and verify again.

conditions:
  - all:
      - key: "{{ buildType }}"
        operator: Equals
        value: "tekton.dev/v1beta1/PipelineRun"
        message: "The buildType must be equal to tekton.dev/v1beta1/PipelineRun, not {{ buildType }}"

      - key: "{{ materials[?starts_with(uri, 'https://gitlab.com/')] | length(@) }}"
        operator: GreaterThan
        value: 0
        message: "The materials must have at least one entry starts with https://gitlab.com/, {{ materials }}"
$ kubectl run -n $NAMESPACE unbuilt-from-specific-repo --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-from-specific-repo was blocked due to the following policies

verify-code-repository-material:
  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-3:latest and predicate https://slsa.dev/provenance/v0.2:
    The materials must have at least one entry starts with https://gitlab.com/,
    [{"digest":{"sha256":"bad5d84ded24307d12cacc9ef37fc38bce90ea5d00501f43b27d0c926be26f19"},"uri":"oci://<registry>/devops/tektoncd/hub/run-script"},{"digest":{"sha256":"7a63e6c2d1b4c118e9a974e7850dd3e9321e07feec8302bcbcd16653c512ac59"},"uri":"http://tekton-hub-api.tekton-pipelines:8000/v1/resource/catalog/task/run-script/0.1/yaml"},{"digest":{"sha256":"8d5ea9ecd9b531e798fecd87ca3b64ee1c95e4f2621d09e893c58ed593bfd4c4"},"uri":"oci://<registry>/devops/tektoncd/hub/buildah"},{"digest":{"sha256":"3225653d04c223be85d173747372290058a738427768c5668ddc784bf24de976"},"uri":"http://tekton-hub-api.tekton-pipelines:8000/v1/resource/catalog/task/buildah/0.9/yaml"},{"digest":{"sha1":"cccccaaaa0000000000000000000000000000000"},"uri":"https://github.com/tektoncd/pipeline"}]'

Step 10: Clean up the resources

Delete the Pods created in the previous steps.

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

Delete the policy.

$ kubectl delete clusterpolicy verify-code-repository-material

Expected Results

After completing this guide:

  • You have a working setup with Tekton Chains for SLSA Provenance generation and Kyverno for source code repository verification
  • Your container images automatically include source code repository information in their SLSA Provenance
  • Only images built from allowed source code repositories can be deployed in the specified namespace
  • Images built from unauthorized source code repositories are automatically blocked by Kyverno policies
  • You have implemented a basic supply chain security control by verifying the source code origins of your container images

This guide provides a foundation for implementing supply chain security 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