10

Tekton Chains

A pushed image is just bytes with a digest. Nothing in it says which pipeline built it, from which commit, with which base images. Tekton Chains fills that gap. It watches runs finish, works out which artifacts they produced, writes provenance in the in-toto and SLSA formats, signs it, and stores it where verifiers can find it. This chapter follows that loop, takes the provenance apart field by field, and then tries to cheat it.

From finished TaskRun to signed attestation

tekton-chainstransparency-log
Chains is a separate controller and acts only after a run completes. Its progress shows up as annotations on the TaskRun, and its output goes to the storage backends you configured.
inject

the Chains controller loop

stage 1/8
TaskRunChainsprovenancesignerstorageRekor
1. TaskRun running

The build runs as usual. Chains plays no part in the build itself, so nothing about the Pod changes.

chains.tekton.dev/signed
—
retries
0
Chains gives up after 3
subject
—
TaskRun (kubectl get -o yaml)
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
name: build-app-x7k2q
uid: 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….sig
    image signature (simplesigning)
  • app:sha256-3a0c2e42a6….att
    attestation (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.

Type hinting: how Chains knows what you built

type-hintingimage-url-digestchains-subject
Chains doesn't watch your registry pushes. It reads results with conventional names. Get the names right and the image becomes the subject of the attestation; get them wrong and the attestation describes nothing.

TaskRun status.results (editable)

✓ image pair IMAGE_URL / IMAGE_DIGEST
what Chains writes
{
"_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", … },
], …
}
}
subjects
1
OCI images
1
get .sig/.att in the registry
inputs
0
from *ARTIFACT_INPUTS

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.

Every field in the predicate comes from something the controller observed: the run's spec, its status, the resolver's refSource and the step image digests. Hover a field to see where it came from.
artifacts.taskrun.format
hover or tap a line: its source in the TaskRun lights up
TaskRun build-app-x7k2q (source of truth)
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
name: build-app-x7k2q
uid: 4b1e0c2a-91f3-4c8e-a2b0-6d1f0e9b7c55
spec:
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: git
params: [url: …/acme/tasks.git, revision: main, pathInRepo: kaniko.yaml]
status:
startTime: 2026-10-07T09:12:03Z
completionTime: 2026-10-07T09:14:41Z
provenance:
refSource:
uri: git+https://github.com/acme/tasks.git
digest: { sha1: 085154084c74… }
entryPoint: kaniko.yaml
featureFlags: { 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
in-toto Statement · SLSA v0.2
{
"_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).

The format names are confusing. In chains-config, slsa/v1 (alias in-toto) produces a SLSA v0.2 predicate. The SLSA v1.0 predicate comes from the slsa/v2alpha* formats (v2alpha4 in current releases).
Two choices decide how much an attestation is worth: what signs it (a key in a Secret, a KMS key, or a short-lived Fulcio certificate) and where it is stored (on the run, next to the image, or in an external store). Both are lines in chains-config.
signer
format
TaskRun attestation storage

where the signature comes from and where it lands

Chainstekton-chainsSecret signing-secretscosign.key (long-lived)Rekor logdisabledtektonTaskRun annotationsociimage registry
chains-config
apiVersion: v1
kind: ConfigMap
metadata: { name: chains-config, namespace: tekton-chains }
data:
artifacts.taskrun.format: slsa/v1
artifacts.taskrun.storage: tekton
artifacts.taskrun.signer: x509
artifacts.oci.format: simplesigning
artifacts.oci.storage: oci
artifacts.oci.signer: x509
transparency.enabled: "false"

cosign / x509 key in a Secret

  1. 1. cosign generate-key-pair k8s://tekton-chains/signing-secrets
  2. 2. Secret holds cosign.key, cosign.password, cosign.pub
  3. 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.

Before deploying, a consumer resolves the image to its digest, fetches the attestation stored for that digest, checks the signature and then the contents. Tamper with any piece and see which check catches it.
after the build
verify with

image in the registry

sha256 (real)
46524f4d20736372617463680a434f505920617070202f61…
digest now sha256:8aa0fc02e02d3a4aefcac1e81820930b164e1fdad63dcf0b5a657a92ef4239b8
attested sha256:8aa0fc02e02d3a4aefcac1e81820930b164e1fdad63dcf0b5a657a92ef4239b8

cosign verify-attestation, step by step

  1. ✓resolve ghcr.io/acme/app:latest → sha256:8aa0fc02e02d3a4a…
  2. ✓found sha256-8aa0fc02e0….att
  3. ✓DSSE signature verifies with chains.pub
  4. ✓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.

terminal
$ cosign verify-attestation --key chains.pub \
--type slsaprovenance ghcr.io/acme/app:latest
Verification 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…"}
ClusterImagePolicy (Sigstore policy-controller)
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata: { name: require-chains-provenance }
spec:
images: [{ glob: "ghcr.io/acme/**" }]
authorities:
- key: { secretRef: { name: chains-pub } }
attestations:
- name: must-have-slsa
predicateType: slsaprovenance
policy:
type: cue
data: 'predicate: builder: id: "https://tekton.dev/chains/v2"'
cosign
verified
kubectl apply -n prod
admitted
policy satisfied

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?

slsa
SLSA's build track asks for more than a signed file. Some requirements are met by Chains itself. Others depend on how you run Tekton: who can reach the signing key, whether runs are isolated, and who can edit run objects.

your setup

SLSA v1.0 build track

  1. Build L3

    Hardened platform: runs can't affect each other and user-defined steps can't forge or influence provenance.

  2. Build L2✓

    Signed provenance from a hosted build platform. Forging it means compromising the platform or its key.

  3. Build L1✓

    Provenance exists: it shows how the artifact was built. Catches mistakes, not attacks.

  4. Build L0✓

    No guarantees.

you reach
Build L2
for L3: runs isolated from each other; signing key unreachable from steps

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.

Chains signs what the TaskRun says. The subject digest comes from a result your own step wrote. SLSA allows the build to report its outputs, but this is why the rest has to be out of the steps' reach: the key, the run's status, and other tenants' runs.
  • 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
  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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