Getting Started Supply Chain Security

Create and sign artifact provenance with Tekton Chains

This guide shows you how to:

  • Create a Pipeline to build and push a container image to a local registry.
  • Record and sign provenance of the image.
  • Read back the provenance information.
  • Verify the signature.

Prerequisites

  1. Install minikube. Only complete the step 1, “Installation”.
  2. Install kubectl.
  3. Install tkn, the Tekton CLI.
  4. Install jq.
  5. Install cosign.

Start minikube with a local registry enabled

  1. Delete any previous clusters:

    minikube delete
    
  2. Start up minikube with insecure registry enabled:

    minikube start --insecure-registry "10.0.0.0/24"
    

    The process takes a few seconds, you see an output similar to the following, depending on the minikube driver that you are using:

    😄  minikube v1.36.0 on Darwin 15.5 (arm64)
    ✨  Using the qemu2 driver based on existing profile
    👍  Starting "minikube" primary control-plane node in "minikube" cluster
    🏃  Updating the running qemu2 "minikube" VM ...
    📦  Preparing Kubernetes v1.33.1 on containerd 1.7.23 ...
    🔗  Configuring bridge CNI (Container Networking Interface) ...
    🔎  Verifying Kubernetes components...
        ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
    🌟  Enabled addons: default-storageclass, storage-provisioner
    🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
    
  3. Enable the local registry plugin:

    minikube addons enable registry 
    

    The output confirms that the registry plugin is enabled:

    💡  registry is an addon maintained by minikube. For any concerns contact minikube on GitHub.
    You can view the list of minikube maintainers at: https://github.com/kubernetes/minikube/blob/master/OWNERS
        ▪ Using image gcr.io/k8s-minikube/kube-registry-proxy:0.0.9
        ▪ Using image docker.io/registry:3.0.0
    🔎  Verifying registry addon...
    🌟  The 'registry' addon is enabled
    

Now you can push images to a registry within your minikube cluster.

Install and configure the necessary Tekton components

  1. Install Tekton Pipelines:

    kubectl apply --filename \
    https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml
    
  2. Monitor the installation:

    kubectl get po -n tekton-pipelines -w
    

    When both tekton-pipelines-controller and tekton-pipelines-webhook show 1/1 under the READY column, you are ready to continue. For example:

    NAME                                          READY   STATUS    RESTARTS   AGE
    tekton-events-controller-786b59d5cd-jt7d9     1/1     Running   0          2m
    tekton-pipelines-controller-59b6cdbbc-2kw2w   1/1     Running   0          2m
    tekton-pipelines-webhook-74b5cdfcc4-g4qj2     1/1     Running   0          2m
    

    Hit Ctrl + C to stop monitoring.

  3. Install Tekton Chains:

    kubectl apply --filename \
    https://storage.googleapis.com/tekton-releases/chains/latest/release.yaml
    
  4. Monitor the installation

    kubectl get po -n tekton-chains -w
    

    When tekton-chains-controller shows 1/1 under the READY column, you are ready to continue. For example:

    NAME                                        READY   STATUS    RESTARTS   AGE
    tekton-chains-controller-7dccbf8fc7-9wdkl   1/1     Running   0          38s
    

    Hit Ctrl + C to stop monitoring.

  5. Configure Tekton Chains to store the provenance metadata locally:

    kubectl patch configmap chains-config -n tekton-chains \
    -p='{"data":{"artifacts.oci.storage": "", "artifacts.taskrun.format":"in-toto", "artifacts.taskrun.storage": "tekton"}}'
    

    The output confirms that the configuration was updated successfully:

    configmap/chains-config patched
    
  6. Generate a key pair to sign the artifact provenance:

    cosign generate-key-pair k8s://tekton-chains/signing-secrets
    

    You are prompted to enter a password for the private key. For this guide, leave the password empty and press Enter twice. A public key, cosign.pub, is created in your current directory.

Build and push a container image

  1. Create a file called pipeline.yaml and add the following:

    apiVersion: tekton.dev/v1
    kind: Pipeline
    metadata:
      name: build-push
    spec:
      params:
        - name: image-reference
          type: string
      results:
        - name: image-ARTIFACT_OUTPUTS
          description: Built artifact.
          value:
            uri: $(tasks.kaniko-build.results.IMAGE_URL)
            digest: sha1:$(tasks.kaniko-build.results.IMAGE_DIGEST)
      workspaces:
        - name: shared-data
      tasks:
        - name: dockerfile
          taskRef:
            name: create-dockerfile
          workspaces:
            - name: source
              workspace: shared-data
        - name: kaniko-build
          runAfter: ["dockerfile"]
          taskRef:
            name: kaniko
          workspaces:
            - name: source
              workspace: shared-data
          params:
            - name: IMAGE
              value: $(params.image-reference)
    ---
    apiVersion: tekton.dev/v1
    kind: Task
    metadata:
      name: create-dockerfile
    spec:
      workspaces:
        - name: source
      steps:
        - name: add-dockerfile
          workingDir: $(workspaces.source.path)
          image: docker.io/bash:5.3.0@sha256:6a3e1c2ddbdee552cd69ad8244eee84ad6cc00049c338f700ef5ef247be16f7b
          script: |
            cat <<EOF > $(workspaces.source.path)/Dockerfile
            FROM alpine:3.22
            RUN echo "hello world" > hello.log
            EOF        
    ---
    apiVersion: tekton.dev/v1
    kind: Task
    metadata:
      name: kaniko
      labels:
        app.kubernetes.io/version: "0.6"
      annotations:
        tekton.dev/pipelines.minVersion: "0.17.0"
        tekton.dev/categories: Image Build
        tekton.dev/tags: image-build
        tekton.dev/displayName: "Build and upload container image using Kaniko"
        tekton.dev/platforms: "linux/amd64,linux/arm64,linux/ppc64le"
    spec:
      description: >-
        This Task builds a simple Dockerfile with kaniko and pushes to a registry.
        This Task stores the image name and digest as results, allowing Tekton Chains to pick up
        that an image was built & sign it.    
      params:
        - name: IMAGE
          description: Name (reference) of the image to build.
        - name: DOCKERFILE
          description: Path to the Dockerfile to build.
          default: ./Dockerfile
        - name: CONTEXT
          description: The build context used by Kaniko.
          default: ./
        - name: EXTRA_ARGS
          type: array
          default: []
        - name: BUILDER_IMAGE
          description: The image on which builds will run (default is v1.24.0)
          default: gcr.io/kaniko-project/executor:v1.24.0@sha256:4e7a52dd1f14872430652bb3b027405b8dfd17c4538751c620ac005741ef9698
      workspaces:
        - name: source
          description: Holds the context and Dockerfile
        - name: dockerconfig
          description: Includes a docker `config.json`
          optional: true
          mountPath: /kaniko/.docker
      results:
        - name: IMAGE_DIGEST
          description: Digest of the image just built.
        - name: IMAGE_URL
          description: URL of the image just built.
      steps:
        - name: build-and-push
          workingDir: $(workspaces.source.path)
          image: $(params.BUILDER_IMAGE)
          args:
            - $(params.EXTRA_ARGS)
            - --dockerfile=$(params.DOCKERFILE)
            - --context=$(workspaces.source.path)/$(params.CONTEXT) # The user does not need to care the workspace and the source.
            - --destination=$(params.IMAGE)
            - --digest-file=$(results.IMAGE_DIGEST.path)
          # kaniko assumes it is running as root, which means this example fails on platforms
          # that default to run containers as random uid (like OpenShift). Adding this securityContext
          # makes it explicit that it needs to run as root.
          securityContext:
            runAsUser: 0
        - name: write-url
          image: docker.io/bash:5.3.0@sha256:6a3e1c2ddbdee552cd69ad8244eee84ad6cc00049c338f700ef5ef247be16f7b
          script: |
            set -e
            image="$(params.IMAGE)"
            echo -n "${image}" | tee "$(results.IMAGE_URL.path)"        
    
  2. Get your cluster IPs:

    kubectl get service --namespace kube-system
    

    This shows the IPs of the services on your cluster:

    NAME       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                  AGE
    kube-dns   ClusterIP   10.96.0.10      <none>        53/UDP,53/TCP,9153/TCP   48m
    registry   ClusterIP   10.101.134.48   <none>        80/TCP,443/TCP           47m
    

    Save your registry IP, in this case 10.101.134.48, for the next step.

  3. Create a file called pipelinerun.yaml and add the following:

    apiVersion: tekton.dev/v1
    kind: PipelineRun
    metadata:
      generateName: build-push-run-
    spec: 
      pipelineRef:
        name: build-push
      params:
      - name: image-reference
        value: <registry-ip>/tekton-test
      workspaces:
      - name: shared-data
        volumeClaimTemplate:
          spec:
            accessModes:
            - ReadWriteOnce
            resources:
              requests:
                storage: 1Gi
    
    
    

    Replace <registry-ip> with the value from the previous step.

  4. Apply the Pipeline to your cluster:

    kubectl apply -f pipeline.yaml
    

    You see the following output:

    pipeline.tekton.dev/build-push created
    task.tekton.dev/create-dockerfile created
    task.tekton.dev/kaniko created
    
  5. Run the Pipeline:

    kubectl create -f pipelinerun.yaml
    

    A new PipelineRun with a unique name is created:

    pipelinerun.tekton.dev/build-push-run-q22b5 created 
    
  6. Monitor the execution:

    tkn pr logs --last -f
    

    The output shows the Pipeline completed successfully:

    [kaniko-build : build-and-push] 2025/07/21 22:19:13 ERROR failed to get CPU variant os=linux error="getCPUVariant for OS linux: not implemented"
    [kaniko-build : build-and-push] INFO[0000] Retrieving image manifest alpine:3.22
    [kaniko-build : build-and-push] INFO[0000] Retrieving image alpine:3.22 from registry index.docker.io
    [kaniko-build : build-and-push] INFO[0002] Built cross stage deps: map[]
    [kaniko-build : build-and-push] INFO[0002] Retrieving image manifest alpine:3.22
    [kaniko-build : build-and-push] INFO[0002] Returning cached image manifest
    [kaniko-build : build-and-push] INFO[0002] Executing 0 build triggers
    [kaniko-build : build-and-push] INFO[0002] Building stage 'alpine:3.22' [idx: '0', base-idx: '-1']
    [kaniko-build : build-and-push] INFO[0002] Unpacking rootfs as cmd RUN echo "hello world" > hello.log requires it.
    [kaniko-build : build-and-push] INFO[0005] RUN echo "hello world" > hello.log
    [kaniko-build : build-and-push] INFO[0005] Initializing snapshotter ...
    [kaniko-build : build-and-push] INFO[0005] Taking snapshot of full filesystem...
    [kaniko-build : build-and-push] INFO[0005] Cmd: /bin/sh
    [kaniko-build : build-and-push] INFO[0005] Args: [-c echo "hello world" > hello.log]
    [kaniko-build : build-and-push] INFO[0005] Running: [/bin/sh -c echo "hello world" > hello.log]
    [kaniko-build : build-and-push] INFO[0005] Taking snapshot of full filesystem...
    [kaniko-build : build-and-push] INFO[0005] Pushing image to 10.99.166.178/tekton-test
    [kaniko-build : build-and-push] INFO[0007] Pushed 10.99.166.178/tekton-test@sha256:3254d61ef67ceb4dd7906b14bb070c00fa039d70ccebb116f08d4f22127f1cf7
    
    [kaniko-build : write-url] 10.99.166.178/tekton-test
    

Retrieve and verify the artifact provenance

Tekton Chains silently monitored the execution of the PipelineRun. It recorded and signed the provenance metadata, information about the container that the PipelineRun built and pushed.

  1. Get the PipelineRun UID:

    export PR_UID=$(tkn pr describe --last -o  jsonpath='{.metadata.uid}')
    
  2. Fetch the metadata and store it in a JSON file:

    tkn pr describe --last \
    -o jsonpath="{.metadata.annotations.chains\.tekton\.dev/signature-pipelinerun-$PR_UID}" \
    | base64 -d > metadata.json
    
  3. View the provenance:

    cat metadata.json | jq -r '.payload' | base64 -d | jq .
    

    The output contains a detailed description of the build:

    {
      "_type": "https://in-toto.io/Statement/v0.1",
      "predicateType": "https://slsa.dev/provenance/v0.2",
      "predicate": {
        "buildConfig": {
          "tasks": [
            {
              "finishedOn": "2025-07-21T22:19:09Z",
              "invocation": {
                "configSource": {},
                "environment": {
                  "annotations": {
                    "pipeline.tekton.dev/affinity-assistant": "affinity-assistant-d220f05d1d",
                    "pipeline.tekton.dev/release": "18736c3"
                  },
                  "labels": {
                    "app.kubernetes.io/managed-by": "tekton-pipelines",
                    "tekton.dev/memberOf": "tasks",
                    "tekton.dev/pipeline": "build-push",
                    "tekton.dev/pipelineRun": "build-push-run-qpfnf",
                    "tekton.dev/pipelineRunUID": "32553c89-e8a8-4c28-81fd-2425433064c8",
                    "tekton.dev/pipelineTask": "dockerfile",
                    "tekton.dev/task": "create-dockerfile"
                  }
                },
                "parameters": {}
              },
              "name": "dockerfile",
              "ref": {
                "kind": "Task",
                "name": "create-dockerfile"
              },
              "serviceAccountName": "default",
              "startedOn": "2025-07-21T22:19:02Z",
              "status": "Succeeded",
              "steps": [
                {
                  "annotations": null,
                  "arguments": null,
                  "entryPoint": "cat <<EOF > /workspace/source/Dockerfile\nFROM alpine:3.22\nRUN echo \"hello world\" > hello.log\nEOF\n",
                  "environment": {
                    "container": "add-dockerfile",
                    "image": "oci://docker.io/library/bash@sha256:6a3e1c2ddbdee552cd69ad8244eee84ad6cc00049c338f700ef5ef247be16f7b"
                  }
                }
              ]
            },
            {
              "after": [
                "dockerfile"
              ],
              "finishedOn": "2025-07-21T22:19:24Z",
              "invocation": {
                "configSource": {},
                "environment": {
                  "annotations": {
                    "pipeline.tekton.dev/affinity-assistant": "affinity-assistant-d220f05d1d",
                    "pipeline.tekton.dev/release": "18736c3",
                    "tekton.dev/categories": "Image Build",
                    "tekton.dev/displayName": "Build and upload container image using Kaniko",
                    "tekton.dev/pipelines.minVersion": "0.17.0",
                    "tekton.dev/platforms": "linux/amd64,linux/arm64,linux/ppc64le",
                    "tekton.dev/tags": "image-build"
                  },
                  "labels": {
                    "app.kubernetes.io/managed-by": "tekton-pipelines",
                    "app.kubernetes.io/version": "0.6",
                    "tekton.dev/memberOf": "tasks",
                    "tekton.dev/pipeline": "build-push",
                    "tekton.dev/pipelineRun": "build-push-run-qpfnf",
                    "tekton.dev/pipelineRunUID": "32553c89-e8a8-4c28-81fd-2425433064c8",
                    "tekton.dev/pipelineTask": "kaniko-build",
                    "tekton.dev/task": "kaniko"
                  }
                },
                "parameters": {
                  "BUILDER_IMAGE": "gcr.io/kaniko-project/executor:v1.24.0@sha256:4e7a52dd1f14872430652bb3b027405b8dfd17c4538751c620ac005741ef9698",
                  "CONTEXT": "./",
                  "DOCKERFILE": "./Dockerfile",
                  "EXTRA_ARGS": [],
                  "IMAGE": "10.99.166.178/tekton-test"
                }
              },
              "name": "kaniko-build",
              "ref": {
                "kind": "Task",
                "name": "kaniko"
              },
              "results": [
                {
                  "name": "IMAGE_DIGEST",
                  "type": "string",
                  "value": "sha256:3254d61ef67ceb4dd7906b14bb070c00fa039d70ccebb116f08d4f22127f1cf7"
                },
                {
                  "name": "IMAGE_URL",
                  "type": "string",
                  "value": "10.99.166.178/tekton-test"
                }
              ],
              "serviceAccountName": "default",
              "startedOn": "2025-07-21T22:19:09Z",
              "status": "Succeeded",
              "steps": [
                {
                  "annotations": null,
                  "arguments": [
                    "--dockerfile=./Dockerfile",
                    "--context=/workspace/source/./",
                    "--destination=10.99.166.178/tekton-test",
                    "--digest-file=/tekton/results/IMAGE_DIGEST"
                  ],
                  "entryPoint": "",
                  "environment": {
                    "container": "build-and-push",
                    "image": "oci://gcr.io/kaniko-project/executor@sha256:4e7a52dd1f14872430652bb3b027405b8dfd17c4538751c620ac005741ef9698"
                  }
                },
                {
                  "annotations": null,
                  "arguments": null,
                  "entryPoint": "set -e\nimage=\"10.99.166.178/tekton-test\"\necho -n \"${image}\" | tee \"/tekton/results/IMAGE_URL\"\n",
                  "environment": {
                    "container": "write-url",
                    "image": "oci://docker.io/library/bash@sha256:6a3e1c2ddbdee552cd69ad8244eee84ad6cc00049c338f700ef5ef247be16f7b"
                  }
                }
              ]
            }
          ]
        },
        "buildType": "tekton.dev/v1beta1/PipelineRun",
        "builder": {
          "id": "https://tekton.dev/chains/v2"
        },
        "invocation": {
          "configSource": {},
          "environment": {
            "labels": {
              "tekton.dev/pipeline": "build-push"
            }
          },
          "parameters": {
            "image-reference": "10.99.166.178/tekton-test"
          }
        },
        "materials": [
          {
            "digest": {
              "sha256": "6a3e1c2ddbdee552cd69ad8244eee84ad6cc00049c338f700ef5ef247be16f7b"
            },
            "uri": "oci://docker.io/library/bash"
          },
          {
            "digest": {
              "sha256": "4e7a52dd1f14872430652bb3b027405b8dfd17c4538751c620ac005741ef9698"
            },
            "uri": "oci://gcr.io/kaniko-project/executor"
          }
        ],
        "metadata": {
          "buildFinishedOn": "2025-07-21T22:19:24Z",
          "buildStartedOn": "2025-07-21T22:19:02Z",
          "completeness": {
            "environment": false,
            "materials": false,
            "parameters": false
          },
          "reproducible": false
        }
      }
    }
    
  4. To verify that the metadata hasn’t been tampered with, check the signature with cosign:

    cosign verify-blob-attestation --insecure-ignore-tlog \
    --key k8s://tekton-chains/signing-secrets --signature metadata.json \
    --type slsaprovenance --check-claims=false /dev/null
    

    The output confirms that the signature is valid:

    Verified OK
    

Further reading


Last modified July 22, 2025: feat: refresh getting started (aa6e0f7)