|
| 1 | +--- |
| 2 | +title: "Keyless Security with Tekton Chains" |
| 3 | +linkTitle: "Keyless Security with Chains" |
| 4 | +date: 2026-06-03 |
| 5 | +author: "Anitha Natarajan, Red Hat" |
| 6 | +description: > |
| 7 | + How keyless signing with Tekton Chains produces verifiable provenance for what |
| 8 | + your pipelines build, with no signing key to manage. |
| 9 | +--- |
| 10 | + |
| 11 | +Traditional signing hands you a long-lived private key and a list of problems: where do you store it, how do you rotate it, who gets access, and what happens when it leaks? In an automated CI/CD pipeline that runs hundreds of builds a day, a key sitting in a secret store is both an operational burden and an attractive target. |
| 12 | + |
| 13 | +[Sigstore](https://www.sigstore.dev/) reframes the problem around *identity* instead of *keys*, and [Tekton Chains](https://tekton.dev/docs/chains/) brings that model into your pipelines. This post covers how it works inside Chains, how to enable it, and the caveats that still apply. |
| 14 | + |
| 15 | +## What Tekton Chains is |
| 16 | + |
| 17 | +Tekton Chains is a Kubernetes controller that observes `TaskRun` and `PipelineRun` resources in your cluster. When a run completes, Chains automatically captures what was built, generates signed provenance describing how it was built, signs the result, and optionally records the signing event in a transparency log. You don't add signing steps to your pipelines; Chains runs out-of-band and reacts to completed runs. |
| 18 | + |
| 19 | +It can sign three kinds of subjects: OCI images, `TaskRuns`, and `PipelineRuns`. Provenance is emitted as [in-toto attestations](https://in-toto.io/), which describe the inputs, steps, and outputs of a build in a structured, verifiable format useful for auditing and for satisfying [SLSA](https://slsa.dev/)-style policy requirements. |
| 20 | + |
| 21 | +## The keyless idea |
| 22 | + |
| 23 | +Instead of holding a signing key, a Sigstore client generates an ephemeral key pair for a single signing operation. It presents an OIDC identity token to [Fulcio](https://docs.sigstore.dev/certificate_authority/overview/), Sigstore's free certificate authority. Fulcio verifies the token and issues a short-lived certificate that binds the freshly generated public key to the identity in the token. The client signs the artifact, the signing event is recorded in [Rekor](https://docs.sigstore.dev/logging/overview/) (the transparency log), and the private key is discarded immediately. There is no key to store, rotate, or leak. Verification later relies on the certificate, the recorded identity, and the transparency log entry rather than on a key you have to distribute. |
| 24 | + |
| 25 | +The key point for CI: the OIDC identity doesn't have to be a human — it can be a workload, which is exactly what Tekton Chains uses. |
| 26 | + |
| 27 | +## How keyless works inside Tekton Chains |
| 28 | + |
| 29 | +In keyless mode, Chains does not look for a signing secret. Instead it requests an identity token from the cluster it is running in, then uses that token to obtain a Fulcio certificate for the artifact being signed. This is the same OIDC signing flow [Cosign](https://docs.sigstore.dev/cosign/overview/) uses, but the token comes from the cluster's service account machinery rather than a browser login. |
| 30 | + |
| 31 | +The mechanism that makes this work on managed Kubernetes is the cluster's OIDC provider combined with [projected service account tokens](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#serviceaccount-token-volume-projection). The cluster issues a short-lived, audience-scoped token representing the workload's service account identity, and Fulcio accepts that token as proof of identity. This has been tested on GKE, EKS, and AKS, and should work on any environment that supports Cosign OIDC signing. |
| 32 | + |
| 33 | +The examples below target the public Sigstore deployment (`fulcio.sigstore.dev` and `rekor.sigstore.dev`), but the flow is identical against a private deployment: many enterprise and on-prem setups run their own Fulcio and Rekor and simply point `signers.x509.fulcio.address` and `transparency.url` at those instances. See the [public vs. private infrastructure](#caveats) caveat below for the trade-offs. |
| 34 | + |
| 35 | +The full flow looks like this: |
| 36 | + |
| 37 | +```mermaid |
| 38 | +flowchart TD |
| 39 | + A["PipelineRun / TaskRun completes"] --> B["Tekton Chains controller<br/>detects completed run"] |
| 40 | + B --> D["Request projected<br/>service account token<br/>from cluster OIDC provider"] |
| 41 | + D --> D2["Generate ephemeral key pair"] |
| 42 | + D2 --> E["Present OIDC token + public key to Fulcio"] |
| 43 | + E --> F["Fulcio verifies identity and issues<br/>short-lived certificate<br/>binding the public key"] |
| 44 | + F --> C["Generate provenance<br/>(in-toto attestation)"] |
| 45 | + C --> G["Sign payload with<br/>ephemeral private key"] |
| 46 | + G --> I["Discard ephemeral private key"] |
| 47 | + G --> K["Store signature, certificate, and payload<br/>(push to OCI registry if configured)"] |
| 48 | + K --> H["Record signing event in Rekor<br/>(if transparency logging is on)"] |
| 49 | + H --> J["Write signed + transparency<br/>annotations on the run"] |
| 50 | +
|
| 51 | + style A fill:#e8f0fe,stroke:#4285f4,color:#000 |
| 52 | + style B fill:#e8f0fe,stroke:#4285f4,color:#000 |
| 53 | + style D2 fill:#fef7e0,stroke:#fbbc04,color:#000 |
| 54 | + style F fill:#fce8e6,stroke:#ea4335,color:#000 |
| 55 | + style H fill:#e6f4ea,stroke:#34a853,color:#000 |
| 56 | + style I fill:#fef7e0,stroke:#fbbc04,color:#000 |
| 57 | +``` |
| 58 | + |
| 59 | +Once Chains has obtained a certificate and signed the payload — the in-toto attestation for a `TaskRun` or `PipelineRun`, or the image manifest for an OCI subject — it marks the run with a `chains.tekton.dev/signed: true` annotation and, if transparency logging is on, a `chains.tekton.dev/transparency` URL pointing at the Rekor entry. The signature, certificate, and payload themselves go wherever you configured storage: base64-encoded annotations on the run with the default `tekton` backend, or pushed to your registry with the `oci` backend. |
| 60 | + |
| 61 | +## Turning it on |
| 62 | + |
| 63 | +Enabling keyless mode is a one-line change to the Chains config map. You switch on the Fulcio signer: |
| 64 | + |
| 65 | +```shell |
| 66 | +kubectl patch configmap chains-config -n tekton-chains \ |
| 67 | + -p='{"data":{"signers.x509.fulcio.enabled": "true"}}' |
| 68 | +``` |
| 69 | + |
| 70 | +A more complete configuration that signs in-toto provenance, stores signatures in your OCI registry, and records events in the public Rekor instance looks like this: |
| 71 | + |
| 72 | +```yaml |
| 73 | +apiVersion: v1 |
| 74 | +kind: ConfigMap |
| 75 | +metadata: |
| 76 | + name: chains-config |
| 77 | + namespace: tekton-chains |
| 78 | +data: |
| 79 | + # What to sign and how to format it |
| 80 | + artifacts.taskrun.format: "in-toto" |
| 81 | + artifacts.taskrun.storage: "oci" |
| 82 | + artifacts.taskrun.signer: "x509" |
| 83 | + |
| 84 | + # Sign PipelineRuns too — provenance pushed to the OCI registry |
| 85 | + artifacts.pipelinerun.format: "in-toto" |
| 86 | + artifacts.pipelinerun.storage: "oci" |
| 87 | + artifacts.pipelinerun.signer: "x509" |
| 88 | + |
| 89 | + # OCI image signature storage |
| 90 | + artifacts.oci.storage: "oci" |
| 91 | + artifacts.oci.format: "simplesigning" |
| 92 | + artifacts.oci.signer: "x509" |
| 93 | + |
| 94 | + # Transparency log |
| 95 | + transparency.enabled: "true" |
| 96 | + transparency.url: "https://rekor.sigstore.dev" |
| 97 | + |
| 98 | + # Keyless: use Fulcio instead of a stored key |
| 99 | + signers.x509.fulcio.enabled: "true" |
| 100 | + signers.x509.fulcio.address: "https://fulcio.sigstore.dev" |
| 101 | +``` |
| 102 | +
|
| 103 | +On a supported managed cluster the only setting keyless strictly requires is `signers.x509.fulcio.enabled: "true"`; the `address` line above just spells out the public-Sigstore default. Note that the example deliberately does **not** set `signers.x509.fulcio.issuer`. That value defaults to the public Sigstore broker (`https://oauth2.sigstore.dev/auth`) and is the *expected OIDC issuer* — you only override it when you run a private OIDC provider or a self-hosted Fulcio, in which case it must be your cluster's own OIDC issuer rather than a Sigstore URL. You can look that up with: |
| 104 | + |
| 105 | +```shell |
| 106 | +kubectl get --raw /.well-known/openid-configuration | jq -r .issuer |
| 107 | +``` |
| 108 | + |
| 109 | +How does Chains know what a run produced? It inspects the completed `TaskRun` or `PipelineRun` — its parameters, steps, and especially its results. *Type-hinting* results such as `IMAGE_URL` and `IMAGE_DIGEST` (or `ARTIFACT_OUTPUTS`) are how your `Task` tells Chains which image it built, and that feeds both the provenance and where the signature lands. It matters for the storage choice above: with `storage: "oci"` and no `storage.oci.repository` set, Chains stores the attestation alongside that image, which only works if those type-hinting results are present. If your run doesn't build an image, set `storage.oci.repository` explicitly or choose a different backend. The [Chains configuration reference](https://tekton.dev/docs/chains/config/) lists every storage option, and the [Artifact Storage in Tekton Chains](/blog/2026/artifact-storage-tekton-chains/) post walks through them in depth. |
| 110 | + |
| 111 | +Transparency logging is its own toggle, defaulting to the public Rekor instance, and you can point it at a private Rekor if you run one: |
| 112 | + |
| 113 | +```shell |
| 114 | +kubectl patch configmap chains-config -n tekton-chains \ |
| 115 | + -p='{"data":{"transparency.enabled": "true"}}' |
| 116 | +
|
| 117 | +kubectl patch configmap chains-config -n tekton-chains \ |
| 118 | + -p='{"data":{"transparency.url": "<YOUR URL>"}}' |
| 119 | +``` |
| 120 | + |
| 121 | +Note that signatures and provenance still need to be pushed somewhere. Chains looks for OCI registry credentials in the pod running your task and in the service account configured for it, so you'll typically create a `dockerconfigjson` secret and attach it to that service account. |
| 122 | + |
| 123 | +## Verifying what you signed |
| 124 | + |
| 125 | +Signing is pointless if no one verifies the result. The question that matters isn't whether something was signed — Chains signs everything — but whether a *consumer* can verify that an artifact came from where they expect before trusting it. Because keyless signing records every event in Rekor, that verification doesn't depend on holding a key; it depends on matching the recorded identity against what you expect and confirming the artifact's entry is in the log. |
| 126 | + |
| 127 | +Concretely, a consumer verifies that the signing certificate's identity is the build workload they trust — not merely *some* valid Sigstore identity — that the signature covers the artifact in hand, and that the attached provenance describes the build they expected. The transparency annotation on each run holds the Rekor entry URL, and you can explore entries through Rekor's API or a web UI; the Chainguard team runs a public [Rekor Search UI](https://rekor.tlog.dev/) where you can look up entries by log index, email, hash, commit SHA, or UUID. Producers can use that same searchability to watch for unexpected use of their identity, but that monitoring is a secondary benefit — the real payoff is giving consumers something concrete to check against policy. |
| 128 | + |
| 129 | +## Signing options today |
| 130 | + |
| 131 | +Keyless is one of several signing modes. Operationally you choose one: |
| 132 | + |
| 133 | +- **Keyless / Fulcio** — no stored key; identity comes from the cluster's OIDC token. The focus of this post. |
| 134 | +- **x509** — an unencrypted PKCS8 PEM private key (`ed25519` or `ecdsa`) stored in a `signing-secrets` Kubernetes secret. |
| 135 | +- **Cosign keypair** — an encrypted Cosign-generated key (`cosign.key` plus `cosign.password`) in the same secret, created via `cosign generate-key-pair k8s://tekton-chains/signing-secrets`. |
| 136 | +- **KMS** — a key held in a cloud KMS, referenced with a go-cloud-style URI using one of the supported schemes: `gcpkms://`, `awskms://`, `azurekms://`, or `hashivault://`. |
| 137 | + |
| 138 | +One subtlety this hides: the `signer` field in the config only ever takes `x509` or `kms` per artifact type (or `none` to skip signing). The first three options above are all the `x509` signer; Chains tells keyless, a raw x509 key, and a Cosign keypair apart by whether `signers.x509.fulcio.enabled` is set and which keys exist in the `signing-secrets` secret, not by a separate top-level setting. (That's also why there's no `cosign` signer value — a Cosign keypair is just the `x509` signer reading `cosign.key`.) |
| 139 | + |
| 140 | +The progression runs from "least key management" to "most control over the key": keyless removes key management entirely, while KMS keeps the key in cloud infrastructure you control. Keyless suits public, auditable artifacts; KMS suits regulatory or air-gap constraints. |
| 141 | + |
| 142 | +## What's possible right now — and the caveats {#caveats} |
| 143 | + |
| 144 | +With keyless enabled, every `TaskRun` and `PipelineRun` in the cluster gets signed provenance bound to a workload identity, with no key to manage and a public audit trail in Rekor — from a single config flag on managed Kubernetes. |
| 145 | + |
| 146 | +A few realities to plan around: |
| 147 | + |
| 148 | +- **OIDC token format matters.** Chains v0.25.1 and later ship Cosign v2.6.0 or newer (the current release line vendors v2.6.3), which no longer accepts HS256-signed JWTs. Public Sigstore (Fulcio), key-based signing, and private OIDC providers using RS256 are unaffected, but if you run a private OIDC provider on HS256 you must switch to RS256 before upgrading to v0.25.1 or above. This is the kind of detail that silently breaks a pipeline, so check it before you bump versions. |
| 149 | +- **Cluster OIDC is a prerequisite.** Keyless mode depends on your cluster being able to issue projected service account tokens that Fulcio trusts. On EKS, for example, that means creating the cluster with OIDC enabled (`eksctl create cluster --with-oidc ...`). On a cluster without a usable OIDC provider, keyless won't work and you'll fall back to a key-based mode. |
| 150 | +- **"Keyless" is a convenience, not the literal absence of keys.** Fulcio still operates a root of trust on behalf of the whole community, protected by a distributed key ceremony and [The Update Framework](https://theupdateframework.io/). The point isn't that keys vanish — it's that *you* no longer manage signing keys. |
| 151 | +- **Public vs. private infrastructure is a real choice.** Signing against the public Fulcio and Rekor means your identities and signing metadata land in public logs, which may be undesirable for internal-only artifacts. You can self-host Fulcio and Rekor to run an entirely private signing stack. If your workloads carry [SPIFFE/SVID](https://spiffe.io/) identities, Chains can authenticate to Fulcio with an SVID instead of a service account token. Either way, running your own stack is meaningfully more work than flipping a flag. |
| 152 | +- **A signature is not a safety verdict.** Chains signs every run unconditionally — it cannot, and does not, judge whether what was built is any good. A signature and its provenance record *who* built an artifact and *how*, not *whether it should be trusted*; on their own they are closer to an empty attestation than a stamp of approval. A compromised build emits perfectly valid signatures and provenance, which is exactly how recent attacks shipped malicious npm packages carrying legitimate provenance. Treat Chains' output as verifiable *input* to a trust decision, never as the decision itself. |
| 153 | +- **Verification and policy are on you.** Chains makes signing automatic; it does not make anyone check the result. The value only materializes when a consumer enforces expectations — a policy gate such as Sigstore's [Policy Controller](https://docs.sigstore.dev/policy-controller/overview/) that admits an artifact only when it was signed by the *specific* build identity you expect, from the source you expect, with provenance meeting the [SLSA](https://slsa.dev/) level you require. Signing without that gate proves an artifact's origin to no one in particular. |
| 154 | + |
| 155 | +## Where this fits |
| 156 | + |
| 157 | +Keyless signing with Tekton Chains gives you verifiable provenance for what your pipeline builds, attributed to a workload identity, with no signing key to manage. That provenance records how and by what each artifact was built. It does not assert the artifact is safe to use — that is a separate decision a consumer makes by verifying the provenance against policy. |
| 158 | + |
| 159 | +Chains is one piece of the supply chain toolchain, and each piece has a distinct job: |
| 160 | + |
| 161 | +- **[in-toto](https://in-toto.io/)** is the *format* — the structured, signable attestation that carries the provenance. |
| 162 | +- **[SLSA](https://slsa.dev/)** is the *standard* — it defines what trustworthy provenance must contain and ranks build integrity in levels you can target and require. |
| 163 | +- **[Policy Controller](https://docs.sigstore.dev/policy-controller/overview/)** is the *enforcement* — admission-time checks that verify those signatures and attestations against your policy before an artifact is deployed. |
| 164 | + |
| 165 | +Signing is necessary but not sufficient. Tekton Chains gives you auditable provenance bound to workload identities; turning that into protection means a consumer closing the loop with verification and policy. Every piece you need for that exists and is open source today. |
| 166 | + |
| 167 | +To go deeper, see the [Tekton Chains documentation](https://tekton.dev/docs/chains/) and the [Sigstore project documentation](https://docs.sigstore.dev/). |
0 commit comments