Tekton Chains
the Chains controller loop
stage 1/8The build runs as usual. Chains plays no part in the build itself, so nothing about the Pod changes.
apiVersion: tekton.dev/v1kind: TaskRunmetadata:name: build-app-x7k2quid: 4b1e0c2a-…annotations: {}status:conditions: [{ type: Succeeded, status: "Unknown" }]results:- { name: IMAGE_URL, value: ghcr.io/acme/app }- { name: IMAGE_DIGEST, value: "sha256:3a0c2e42a6869e7b…" }
registry: ghcr.io/acme
- app:latest→ sha256:3a0c2e42a6…
- app:sha256-3a0c2e42a6….sigimage signature (simplesigning)
- app:sha256-3a0c2e42a6….attattestation (DSSE in-toto)
cosign’s naming convention: the digest becomes a tag with a .sig or .att suffix in the same repository.
Try: step through and watch the annotations appear on the TaskRun after it has finished. Then inject “signing-secrets empty”: Chains retries three times and marks the run signed=failed, while the image itself was pushed fine.
TaskRun status.results (editable)
{"_type": "https://in-toto.io/Statement/v0.1","predicateType": "https://slsa.dev/provenance/v0.2","subject": [{ "name": "ghcr.io/acme/app","digest": { "sha256": "a172cedcae47474b…" } }],"predicate": {"materials": [{ "uri": "oci://gcr.io/kaniko-project/executor", … },], …}}
Try: load the misnamed preset. The build is identical, but Chains finds no subject, so the attestation describes nothing. Then rename IMAGE_REF to IMAGE_URL and DIGEST to IMAGE_DIGEST and watch the subject appear. The truncated digest shows the format matters too.
apiVersion: tekton.dev/v1kind: TaskRunmetadata:name: build-app-x7k2quid: 4b1e0c2a-91f3-4c8e-a2b0-6d1f0e9b7c55spec:params:- { name: IMAGE, value: ghcr.io/acme/app }- { name: CHAINS-GIT_URL, value: https://github.com/acme/app }- { name: CHAINS-GIT_COMMIT, value: 439a6db817da… }taskRef:resolver: gitparams: [url: …/acme/tasks.git, revision: main, pathInRepo: kaniko.yaml]status:startTime: 2026-10-07T09:12:03ZcompletionTime: 2026-10-07T09:14:41Zprovenance:refSource:uri: git+https://github.com/acme/tasks.gitdigest: { sha1: 085154084c74… }entryPoint: kaniko.yamlfeatureFlags: { enableAPIFields: beta, … }steps:- { name: build-and-push, imageID: gcr.io/kaniko-project/executor@sha256:380dd65a0b7b… }- { name: write-url, imageID: docker.io/library/bash@sha256:37d2b12d5d9a… }results:- { name: IMAGE_URL, value: ghcr.io/acme/app }- { name: IMAGE_DIGEST, value: sha256:a172cedcae47… }# chains-config: builder.id: https://tekton.dev/chains/v2
{"_type": "https://in-toto.io/Statement/v0.1","predicateType": "https://slsa.dev/provenance/v0.2","subject": [{"name": "ghcr.io/acme/app","digest": { "sha256": "a172cedcae47…" } }],"predicate": {"builder": { "id": "https://tekton.dev/chains/v2" },"buildType": "tekton.dev/v1/TaskRun","invocation": {"configSource": {"uri": "git+https://github.com/acme/tasks.git","digest": { "sha1": "085154084c74…" },"entryPoint": "kaniko.yaml" },"parameters": { "IMAGE": "ghcr.io/acme/app", … }},"buildConfig": { "steps": [ … entryPoint, arguments, image … ] },"metadata": {"buildStartedOn": "2026-10-07T09:12:03Z","buildFinishedOn": "2026-10-07T09:14:41Z","completeness": { "parameters": false, … }, "reproducible": false },"materials": [{ "uri": "oci://gcr.io/kaniko-project/executor", "digest": { "sha256": "380dd65a0b7b…" } },{ "uri": "oci://docker.io/library/bash", "digest": { "sha256": "37d2b12d5d9a…" } },{ "uri": "git+https://github.com/acme/app.git", "digest": { "sha1": "439a6db817da…" } }]}}
subject
What was built, with its digest. Comes from the type-hinted results IMAGE_URL and IMAGE_DIGEST. A verifier matches the image it is about to deploy against this digest.
Try: hover the configSource / task dependency. The TaskRun asked for revision: main, but the provenance records the commit SHA from refSource. Then switch formats: the same facts move from materials/invocation (v0.2) to resolvedDependencies/externalParameters (v1.0).
where the signature comes from and where it lands
apiVersion: v1kind: ConfigMapmetadata: { name: chains-config, namespace: tekton-chains }data:artifacts.taskrun.format: slsa/v1artifacts.taskrun.storage: tektonartifacts.taskrun.signer: x509artifacts.oci.format: simplesigningartifacts.oci.storage: ociartifacts.oci.signer: x509transparency.enabled: "false"
cosign / x509 key in a Secret
- 1. cosign generate-key-pair k8s://tekton-chains/signing-secrets
- 2. Secret holds cosign.key, cosign.password, cosign.pub
- 3. Chains mounts it and signs with the private key
The private key is a long-lived file in the cluster. Anyone who can read Secrets in tekton-chains can sign anything.
what each enabled backend holds
- tekton payload-, signature- and cert-taskrun-<uid> annotations on the run itself. Simple, but gone when the run is pruned.
- oci the attestation as sha256-<digest>.att and the signature as .sig, next to the image. Travels with the image; what cosign looks up.
Try: switch to keyless and watch the config demand Fulcio and flag the missing transparency log. Add oci to the TaskRun storage to put the attestation next to the image, where cosign verify-attestation will look for it.
image in the registry
sha256 (real)cosign verify-attestation, step by step
- ✓resolve ghcr.io/acme/app:latest → sha256:8aa0fc02e02d3a4a…
- ✓found sha256-8aa0fc02e0….att
- ✓DSSE signature verifies with chains.pub
- ✓subject digest matches the image
Signature here is a stand-in (sha256 of key ‖ payload) for cosign’s ECDSA; the digests are real SHA-256.
$ cosign verify-attestation --key chains.pub \--type slsaprovenance ghcr.io/acme/app:latestVerification for ghcr.io/acme/app: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":"eyJfdHlwZSI6Imh0dHBzOi8vaW…"}
apiVersion: policy.sigstore.dev/v1beta1kind: ClusterImagePolicymetadata: { name: require-chains-provenance }spec:images: [{ glob: "ghcr.io/acme/**" }]authorities:- key: { secretRef: { name: chains-pub } }attestations:- name: must-have-slsapredicateType: slsaprovenancepolicy:type: cuedata: 'predicate: builder: id: "https://tekton.dev/chains/v2"'
Try: flip one bit of the image. Its digest changes completely, so cosign finds no attestation for it. Edit the provenance and the signature breaks. Re-sign it with your own key and verify with attacker.pub: cosign passes, which is why the key or identity you verify against, pinned in the admission policy, is the real trust anchor.
What SLSA level does this get you?
slsayour setup
SLSA v1.0 build track
- Build L3
Hardened platform: runs can't affect each other and user-defined steps can't forge or influence provenance.
- Build L2✓
Signed provenance from a hosted build platform. Forging it means compromising the platform or its key.
- Build L1✓
Provenance exists: it shows how the artifact was built. Catches mistakes, not attacks.
- Build L0✓
No guarantees.
Try: start from “Chains defaults”. You are at L2: signed provenance from a hosted platform. L3 needs two more things that Chains can’t give you: isolation between runs and a signing key no build step can reach. Then switch off type hints: provenance without a subject doesn’t even reach L1 for that artifact.
- takeawayChains works after the fact. It watches completed runs, reads type-hinted results to find subjects, builds an in-toto/SLSA statement from what the controller observed, signs it, stores it and marks the run signed.
- takeawayThe attestation binds a digest to a build. Change one bit of the image and no attestation matches it. Edit the provenance and the signature breaks. A verifier still has to pin the right key or identity.
- takeawayChains gets you signed provenance from a hosted platform, roughly SLSA Build L2. L3 is a property of your cluster: a key no step can read, runs that can't touch each other, and RBAC that keeps tenants away from run status.
Key concepts · 17
book chapter 10- Tekton Chains
A Kubernetes controller that observes completed TaskRuns and PipelineRuns, generates provenance for their outputs, signs it and stores it.
It adds supply-chain metadata without changing your Tasks: the build stays the same, and Chains works from the run object afterwards. It marks each run chains.tekton.dev/signed=true (or failed) when done.
tekton-chains namespacechains.tekton.dev/signed↑ see it in the visualization - Provenance
Verifiable information about how an artifact was produced: which builder, which build definition and inputs, which dependencies, when.
A digest tells you what an image is; provenance tells you where it came from. Policies can then reject images that were not built by your pipeline from your repo.
↑ see it in the visualization - Attestation
A signed statement about one or more artifacts, here an in-toto Statement wrapped in a DSSE envelope and signed by Chains.
Signing binds the claim to a key or identity, so anyone holding the public key can detect a modified or forged document.
application/vnd.in-toto+jsonDSSE↑ see it in the visualization - in-toto Statement
The attestation envelope format: _type, a list of subjects (name + digest) and a typed predicate.
A standard wrapper means cosign, policy engines and other verifiers can read Chains output without knowing anything about Tekton.
https://in-toto.io/Statement/v0.1https://in-toto.io/Statement/v1↑ see it in the visualization - SLSA provenance and build levels
Supply-chain Levels for Software Artifacts: a provenance predicate schema (v0.2, v1.0) and a build track of levels L0 to L3.
L1 means provenance exists, L2 means it is signed by a hosted platform, and L3 means the platform is hardened so tenants can't forge it. Chains generates and signs provenance; L3 depends on how you run the cluster.
https://slsa.dev/provenance/v0.2https://slsa.dev/provenance/v1↑ see it in the visualization - Subject
An artifact the attestation is about, identified by name and digest (e.g. ghcr.io/acme/app with sha256:…).
Verification starts from the subject: the digest of what you are about to deploy must appear there. With no subject, the attestation vouches for nothing.
↑ see it in the visualization - materials / resolvedDependencies
The inputs of the build: step images by digest, the Task source from refSource, and source repos given via CHAINS-GIT_URL/CHAINS-GIT_COMMIT or *ARTIFACT_INPUTS.
They let a verifier ask “which base image and which commit went into this?” and catch builds from unapproved sources or outdated images. They are called materials in SLSA v0.2 and resolvedDependencies in v1.0.
CHAINS-GIT_URLCHAINS-GIT_COMMIT*ARTIFACT_INPUTS↑ see it in the visualization - Type hinting
Naming conventions for Task results that tell Chains which artifacts a run produced.
Chains does not inspect what your steps push. If results are misnamed or the digest is malformed, the build still succeeds but the attestation has no subject.
IMAGES*_IMAGE_URL / *_IMAGE_DIGEST*_ARTIFACT_URI / *_ARTIFACT_DIGEST*ARTIFACT_OUTPUTS↑ see it in the visualization - IMAGE_URL / IMAGE_DIGEST
The pair of string results catalog build Tasks (kaniko, buildah) already emit: the image reference and its sha256 digest.
Using the standard names gives you provenance for free with catalog Tasks. The digest must be the full sha256:<64 hex> form.
results.IMAGE_URLresults.IMAGE_DIGEST↑ see it in the visualization - Signing key (x509 / cosign)
A key pair generated by cosign into the signing-secrets Secret (cosign.key, cosign.password, cosign.pub) in the tekton-chains namespace.
The simplest setup, but the private key is a long-lived secret. Anyone who can read it, including a build step that somehow can, can sign arbitrary provenance.
cosign generate-key-pair k8s://tekton-chains/signing-secrets↑ see it in the visualization - KMS signer
Signing through a cloud or Vault KMS key referenced by signers.kms.kmsref, so the private key never enters the cluster.
Removes key theft from the threat model and gives an audit log of every signing operation.
gcpkms://awskms://azurekms://hashivault://↑ see it in the visualization - Keyless signing (Fulcio)
Chains proves its workload identity with an OIDC token, Sigstore's Fulcio CA issues a short-lived certificate for an ephemeral key, and the key is discarded after signing.
No long-lived key to protect or rotate. Verifiers check the certificate identity and issuer instead of a public key, and rely on Rekor to show the signature was made while the cert was valid.
signers.x509.fulcio.enabled--certificate-identity--certificate-oidc-issuer↑ see it in the visualization - Rekor transparency log
Sigstore's append-only public log of signatures; with transparency.enabled, Chains uploads each one and annotates the run with the entry.
Makes signing events publicly auditable and timestamped, which keyless verification needs. Mind that public Rekor entries reveal artifact names and digests.
transparency.enabledchains.tekton.dev/transparency↑ see it in the visualization - Storage backends
Where Chains puts signatures and attestations: tekton (run annotations), oci (next to the image in the registry), gcs, docdb, grafeas.
oci storage keeps the attestation with the image, which is where cosign and admission controllers look. tekton storage disappears when old runs are pruned.
artifacts.taskrun.storageartifacts.oci.storagesha256-<digest>.att↑ see it in the visualization - chains-config
The ConfigMap in tekton-chains that sets per-artifact-type format, storage and signer, plus transparency, builder id and signer options.
Everything about Chains' output is configuration, not Task code: switch formats or add a backend without touching a pipeline.
artifacts.taskrun.format: slsa/v1artifacts.oci.storage: ocitransparency.enabled: true↑ see it in the visualization - Verification (cosign verify-attestation)
Resolving the image digest, fetching the attestation for that digest, checking the signature against a key or identity, and checking the subject and predicate.
Flipping one bit of the image changes its digest, and editing the provenance breaks the signature. Neither can be hidden, as long as the verifier pins the right key or identity.
cosign verify-attestation --key cosign.pub --type slsaprovenance↑ see it in the visualization - Deploy-time policy
An admission controller (Sigstore policy-controller, Kyverno verifyImages, Gatekeeper with a provider) that verifies attestations before a Pod is admitted.
Provenance nobody checks protects nothing. The policy is where you require the Chains key and, for example, a specific builder.id or source repo.
ClusterImagePolicyKyverno verifyImages.attestations↑ see it in the visualization