Sigstore & cosign
Sigstore is a free, public set of tools and infrastructure for signing and verifying software artifacts, and cosign is its command-line signer — the tool most teams actually type. Its headline feature is keyless signing: instead of generating a private key, protecting it forever, and distributing the matching public key to every verifier, cosign proves who's signing through an OIDC identity, gets a certificate that's valid for about ten minutes from Sigstore's own certificate authority, signs with it, and then throws the key away — because a public, tamper-evident log called Rekor has already recorded that the signing happened, at that moment, by that identity, permanently. By the end of this page you should be able to explain exactly what happens between running cosign sign and a green checkmark on cosign verify — the OIDC exchange, the Fulcio certificate, the Rekor log entry — and wire a signature check into a Kubernetes admission gate so an unsigned or wrongly-signed image is rejected before it ever runs.
A normal signature is like a stamp with your name carved into it — if someone steals the stamp, they can forge your name forever, and you'd never know until the damage was done. Keyless signing is different: instead of a stamp you keep forever, you go to a notary each time, prove who you are on the spot, and the notary hands you a stamp that only works for the next ten minutes — then writes down in a public logbook, "at 3:04pm, this person, who proved they were them, stamped this exact document." The stamp expires almost immediately. The logbook entry doesn't. A year later, nobody checks whether the stamp still works — they check the logbook.
What Sigstore and cosign are, and the problem keyless signing solves
☺ Like you're 10: Guarding a key forever is hard and risky; proving who you are fresh each time, in public, turns out to be easier and safer.
Sigstore is an umbrella open-source project — now a Linux Foundation project under the Open Source Security Foundation (OpenSSF) — providing free, publicly-run infrastructure for signing software artifacts and verifying who signed them. It came out of work started in 2021, credited primarily to Dan Lorenc (then at Google, later a co-founder of the container-security company Chainguard) together with Luke Hinds at Red Hat, who's generally credited with originating the project. Sigstore bundles several components: cosign, the CLI most people actually use to sign and verify; Fulcio, the certificate authority that issues short-lived signing certificates; Rekor, the transparency log this page spends a whole section on; and gitsign, which applies the same identity-based model to signing git commits instead of container images. This page is about cosign as the tool you run, but "cosign" and "keyless signing" only make sense once you understand the three-legged infrastructure — Fulcio, Rekor, and an OIDC identity provider — that cosign leans on to do it.
cosign can also do traditional, keyed signing — cosign generate-key-pair writes an encrypted cosign.key and a cosign.pub to disk, exactly like a GPG keypair, and cosign can sign with a key held in AWS KMS, GCP KMS, Azure Key Vault, or HashiCorp Vault's transit engine just as easily as a local file. That model works, and some organizations deliberately choose it — see cryptography & key management for when a durable, org-owned key is actually the right call. But it hands the organization a long-lived secret to protect: generate it once, store it somewhere access-controlled (ideally an HSM or a KMS, never a laptop), back it up, rotate it on a schedule, and revoke it everywhere the moment it's suspected of leaking — and every verifier needs the matching public key distributed and kept current. A leaked signing key is quiet and catastrophic: anyone holding it can produce a signature that verifies perfectly, and there's no way to tell a legitimate signature from a forged one until the leak is discovered, if it ever is.
Keyless signing sidesteps that entire lifecycle by never creating a durable key in the first place. Trust shifts from "does this signature match a public key I have stored somewhere" to "was this signed by someone who could prove, at the moment of signing, that they controlled this exact email address or this exact CI workflow" — backed by Fulcio's certificate issuance and made permanent by Rekor's public log, rather than by a keypair anyone has to guard forever.
The cryptography behind keyless signing isn't new — signing with a freshly generated, short-lived key is a well-understood idea. Sigstore's actual contribution is making that ephemeral certificate trustworthy long after it expires, by writing every signing event to a public log the instant it happens. A ten-minute-old certificate would be useless on its own a year later; the permanent Rekor entry is what makes it verifiable indefinitely.
Keyless signing: OIDC identity, Fulcio, and the ephemeral certificate
☺ Like you're 10: You don't carry a permanent ID card around. Each time you need one, you prove who you are on the spot, get a temporary pass good for a few minutes, and use it immediately.
Run cosign sign against a container image and five things happen in quick succession, entirely before anything gets pushed anywhere:
- cosign generates a brand-new ephemeral keypair — an ECDSA key, created in memory, for this one signing operation and nothing else.
- cosign proves an identity through OIDC. Run interactively, it opens a browser to Sigstore's OAuth flow against a trusted identity provider — Google, Microsoft, or GitHub login — and you authenticate there. Run inside CI, there's no browser at all: cosign instead reads an ambient OIDC token the CI platform already issues. On GitHub Actions, that means requesting the
id-token: writepermission on the job — nothing else — and cosign fetches a short-lived ID token from GitHub's own OIDC provider automatically. No signing secret is stored in the repository at any point. - cosign sends a certificate request to Fulcio — the ephemeral public key plus the OIDC ID token, together.
- Fulcio verifies the token and issues a certificate. It checks the ID token's signature against the issuer's own published OIDC keys, extracts the verified identity from its claims — an email address for an interactive login, or for GitHub Actions, a URI naming the exact repository, workflow file, and git ref that's running — and issues a short-lived X.509 certificate binding that identity to the ephemeral public key. The validity window is deliberately short, on the order of ten minutes; Sigstore has tightened this window across releases, so check Fulcio's current default rather than treating that figure as fixed.
- cosign signs, then discards the key. It signs the artifact's content digest with the ephemeral private key that matches the certificate Fulcio just issued, uploads the result, and the private key is gone — it was never written to disk, and there is nothing left afterward for an attacker to steal.
For GitHub Actions specifically, Fulcio embeds more than just a top-level identity string into the certificate. Additional certificate extensions record the exact commit SHA, the triggering event, and the workflow ref that produced the token, which is what lets a verifier pin trust to "signed by exactly this workflow, on this branch" rather than the much looser "signed by something published under this repository." cosign exposes matching verification flags — --certificate-github-workflow-repository, --certificate-github-workflow-ref, and several siblings — for checking against those specific extensions rather than just the top-level subject.
Keyless signing doesn't remove the need to protect something — it moves what needs protecting from a keypair to whatever can obtain that OIDC token. A GitHub Actions workflow that grants id-token: write to a job triggered by pull_request from an untrusted fork hands anyone who can open a PR the exact same ability to mint a validly-signed certificate carrying your repository's identity. Scope id-token: write narrowly, and treat "who can trigger this workflow" as the security boundary it now is — see zero trust for pipelines and workload identity & pipeline IAM for the broader pattern this is one instance of.
Rekor: the transparency log that makes an ephemeral certificate durable
☺ Like you're 10: The pass expires in ten minutes, but the notary's logbook entry proving it was issued, and exactly when, stays around forever — and anyone can go check the logbook.
Here's the problem an ephemeral certificate would create on its own: verify a container image a year after it shipped, and the Fulcio certificate that signed it expired within minutes of being issued. If cosign verify simply checked "is this certificate valid right now," every keyless signature would fail the moment its ten-minute window closed — which would make keyless signing useless for anything long-lived. Rekor is what solves that. When cosign signs, it also uploads the signature, the Fulcio certificate, and the artifact's digest to Rekor, a public, append-only transparency log built on a Merkle tree — the same lineage of design as the Certificate Transparency logs the web's TLS ecosystem already runs on. Rekor returns an inclusion proof — a Signed Entry Timestamp (SET) — cryptographic evidence that this exact entry sits at this exact position in the log, at this exact time.
At verify time, cosign doesn't check the certificate's validity against today's date. It checks that the Rekor entry's recorded timestamp falls inside the certificate's original ten-minute validity window. That's the actual trick: the certificate only ever had to be valid for the few seconds it took to sign, and the permanent, tamper-evident Rekor entry is the durable proof that it was valid then. A signature made a year ago still verifies today, not because the certificate is still valid — it very much isn't — but because the log entry proving it once was hasn't changed and can't be altered without detection.
Rekor is public and independently auditable by design: the hosted public-good instance at rekor.sigstore.dev publishes its Merkle tree root, and anyone — not just Sigstore — can run a consistency check against that root to confirm no entry has been silently altered or quietly removed after the fact. That openness cuts both ways operationally. It means an organization can monitor Rekor for entries carrying its own signing identity, to catch a compromised CI credential signing something nobody on the team actually built — the exact kind of supply-chain tampering that went undetected for months in incidents like SolarWinds. But it also means the identity in the certificate — an email address for an interactive signer, or a private repository's exact path for a GitHub Actions signer — sits in a log the whole internet can read. Organizations that consider that identity information sensitive can self-host a private Fulcio and Rekor instead of using the public-good instances, trading the convenience of shared infrastructure for keeping that record inside their own perimeter. Fulcio separately publishes every certificate it issues to its own certificate-transparency-style log, so identity issuance itself is auditable independently of Rekor's record of the signing event that used it.
Signing, attesting, and verifying: the commands you'll actually run
☺ Like you're 10: Four commands cover almost all of it: sign the thing, attach a fact about the thing, check the signature, check the fact.
# interactive keyless signing — opens a browser for the OIDC login
$ cosign sign registry.acme.io/checkout@sha256:1a2b3c...
Generating ephemeral keys...
Retrieving signed certificate...
Note that there may be personally identifiable information associated with this signed
artifact. This may include the email address associated with the account with which you
authenticate. This information will be used for signing this artifact and will be stored
in public transparency logs and cannot be removed later.
Are you sure you would like to continue? [y/N] y
Your browser will now be opened to: https://oauth2.sigstore.dev/auth/auth?...
Successfully verified SCT...
tlog entry created with index: 145829103
Pushing signature to: registry.acme.io/checkout
# sign against a digest, not a mutable tag — signing a tag signs whatever it points to *right now*
$ cosign sign --yes registry.acme.io/checkout@sha256:1a2b3c...# .github/workflows/release.yml — keyless signing in CI, ambient OIDC, zero secrets
permissions:
id-token: write # this is the whole trick — it's what lets GitHub mint the OIDC token
contents: read
packages: write
jobs:
build-and-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- name: Build and push
id: build
run: |
docker build -t ghcr.io/acme/checkout:${{ github.sha }} .
docker push ghcr.io/acme/checkout:${{ github.sha }}
- name: Sign the image
run: |
# --yes: skip the interactive confirmation prompt — required in non-interactive CI
cosign sign --yes ghcr.io/acme/checkout@${{ steps.build.outputs.digest }}# verify — the identity flags below are NOT optional for keyless verification.
# without them cosign refuses to run, precisely because an unconstrained verify
# would accept a valid signature from *any* keyless signer on the planet.
$ cosign verify \
--certificate-identity="https://github.com/acme/checkout/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/acme/checkout@sha256:1a2b3c... | jq .
# attach and verify a fact ABOUT the image — an SBOM, as an in-toto attestation
$ cosign attest --yes --predicate sbom.cdx.json --type cyclonedx \
ghcr.io/acme/checkout@sha256:1a2b3c...
$ cosign verify-attestation --type cyclonedx \
--certificate-identity="https://github.com/acme/checkout/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/acme/checkout@sha256:1a2b3c...
# the keyed alternative still exists — a durable key backed by a real KMS, not a laptop file
$ cosign generate-key-pair --kms awskms:///alias/acme-image-signing
$ cosign sign --key awskms:///alias/acme-image-signing ghcr.io/acme/checkout@sha256:1a2b3c...cosign sign produces a bare signature over the artifact digest — proof that this exact bytes-on-the-wire came from this identity. cosign attest is a different, related thing: it wraps a predicate — an SBOM, a SLSA provenance statement, a vulnerability-scan result, anything expressible as JSON — inside a signed in-toto statement and attaches it to the artifact, so a verifier can check not just "who built this" but "what does this claim to contain, and is that claim itself signed." See software bills of materials for what actually belongs in that predicate, and pair cosign with Syft or Trivy as the tool that generates it — cosign attests to a document, it doesn't produce one itself.
Verifying provenance at an admission-control gate
☺ Like you're 10: The registry will happily hand an unsigned image to anyone who asks for it — the check has to happen at the cluster's front door, not at the shelf.
A signature nobody checks is a compliance artifact, not a control. Signing the image is half the job; the other half is refusing to run anything that isn't signed by the right identity, and that enforcement point is a Kubernetes admission controller — a webhook the API server calls on every resource creation, before the object is ever persisted or a Pod is ever scheduled. Two common ways to wire cosign's verification logic into that webhook: Kyverno's built-in verifyImages rule, or Sigstore's own purpose-built policy-controller (the project's admission webhook, using a ClusterImagePolicy custom resource). OPA Gatekeeper can enforce the same idea too, though it needs an external data source or a custom provider to reach cosign's verification logic, since Rego itself has no built-in notion of a Sigstore signature.
# Kyverno — reject any Pod whose image isn't signed by exactly this GitHub Actions workflow
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-checkout-image-signature
spec:
validationFailureAction: Enforce # Audit first in a new rollout, then flip to Enforce
webhookTimeoutSeconds: 30 # Fulcio/Rekor round-trips need real headroom
rules:
- name: verify-signature
match:
any:
- resources: { kinds: [Pod] }
verifyImages:
- imageReferences:
- "ghcr.io/acme/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/acme/*/.github/workflows/release.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
# a second verifyImages entry can additionally require an SBOM/SLSA attestation
# to be present and pass, not just a bare signature# Sigstore's own policy-controller — the ClusterImagePolicy CRD, same identity, native fit
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: checkout-image-policy
spec:
images:
- glob: "ghcr.io/acme/**"
authorities:
- keyless:
identities:
- issuer: https://token.actions.githubusercontent.com
subject: https://github.com/acme/checkout/.github/workflows/release.yml@refs/heads/mainWhat actually happens at admission time mirrors cosign verify exactly, just running server-side as a webhook instead of a step someone runs by hand: the webhook fetches the image's attached signature, certificate, and Rekor bundle from the registry, re-derives trust independently — checking the certificate chains to Fulcio's root (distributed to the cluster via TUF and cached, not fetched fresh on every request) and that the Rekor inclusion proof is valid — confirms the identity embedded in the certificate matches the policy's allowed subject and issuer, and only then admits the Pod. Anything that fails any one of those checks — unsigned, signed by the wrong identity, or signed but missing a required attestation — gets rejected before a container ever starts, which is the entire point: a scanner that ran in CI three weeks ago says nothing about whether the exact bytes about to run in production are the ones that scan actually covered. See Part 4 of the capstone lab for wiring this exact gate end to end against a real cluster, and container & supply-chain security for where signing sits in the broader chain of custody.
Gotchas and failure modes
☺ Like you're 10: Most of the surprises come from forgetting that "keyless" still depends on something — a network connection, an exact identity match, or the CI job that's allowed to sign at all.
- An unconstrained
cosign verifyis the classic footgun. Older tutorials, and earlier cosign releases, allowedcosign verifywith no--certificate-identity/--certificate-oidc-issuerflags at all for keyless signatures — which silently accepts a valid signature from any keyless signer anywhere, not just yours. Current cosign requires those flags for keyless verification and errors without them; if you're copying a command from an old blog post or a stale internal wiki page, check it actually pins an identity before trusting the "verified" output. - Signing a tag signs whatever it points to right now, not what you meant. Tags are mutable; a signature made against
checkout:latestsays nothing about whatcheckout:latestpoints to a week later, since the tag can move underneath it. Sign and verify by immutable digest (@sha256:...), always. - Both sign and verify need network access to Fulcio, Rekor, and Sigstore's TUF root distribution — the mechanism that keeps cosign's trusted root (Fulcio's CA certificate, Rekor's public key) current without a separate manual update. Run
cosign initializeahead of time in an air-gapped or heavily restricted environment to fetch that trust root in advance; offline verification bundles (--output-signaturewith the embedded SET) reduce, but don't eliminate, the need to reach that trust material at some point. - The public Rekor instance means public identity. An email address, or a private repository's exact path and workflow name, ends up readable by anyone who queries
rekor.sigstore.dev. For organizations where that's a real information-disclosure concern, self-hosting a private Fulcio and Rekor is the standard mitigation — at the cost of running and trusting your own instance instead of Sigstore's shared public-good one. - Signing protects supply-chain integrity, not code quality. A signature proves an artifact came from the identity it claims and hasn't been altered since — it says nothing about whether that artifact is actually safe to run. A perfectly signed image can still ship a critical CVE or a hardcoded secret; signing is one gate among several, not a substitute for the scanning covered in SAST, DAST & SCA.
- Registries without OCI 1.1 referrers-API support fall back to the older tag convention — cosign attaches a signature as a separate tag like
sha256-<digest>.sigrather than a proper OCI referrer. Both work, but mixing the two across a fleet of registries can confuse tooling that only expects one convention; check what your registry actually supports before assuming referrers-API behavior everywhere.
Sigstore/cosign vs. the alternatives
☺ Like you're 10: Every alternative is really the same choice in disguise — trust a person's identity right now, or trust a key someone's been guarding for years.
| Approach | Trust anchor | Strength | Trade-off |
|---|---|---|---|
| cosign, keyless | OIDC identity + Fulcio cert + Rekor log entry | No long-lived key to generate, store, rotate, or leak; identity maps to exactly who or what CI job signed | Requires network access to Fulcio/Rekor/TUF at sign and (usually) verify time; identity is publicly logged unless self-hosted |
| cosign, keyed (KMS-backed) | A long-lived keypair held in AWS/GCP/Azure KMS or Vault | Works fully offline once the key exists; org retains a durable, org-owned signing identity independent of any individual's OIDC account | Full key-lifecycle burden returns — generation, HSM/KMS custody, rotation, revocation, and public-key distribution to every verifier |
| Notation (CNCF Notary Project, "Notary v2") | A traditional PKI certificate from a CA you trust, via a pluggable signing backend | OCI-native like cosign; plugin model integrates cleanly with an enterprise's existing CA or a cloud vendor's own signer (e.g. AWS Signer, Azure Key Vault) | Defaults to the traditional long-lived-certificate trust model rather than keyless-by-default; smaller ecosystem of ready-made admission-controller integrations than Sigstore's |
| Plain GPG signing | A GPG keypair, manually distributed | Familiar, works everywhere, no dependency on any Sigstore infrastructure | No native OCI-registry integration — signatures live outside the registry as a side artifact you build tooling to check yourself; full manual key-lifecycle burden |
| Docker Content Trust (legacy Notary v1) | A TUF-based root-and-targets key hierarchy | Was the first widely-shipped signing story built into the Docker CLI | Effectively legacy — the underlying Notary v1 server most Content Trust setups relied on is unmaintained; Notation and Sigstore are what replaced it |
In practice, "keyless cosign or a KMS-backed key" is the decision that comes up most often, and it's rarely all-or-nothing: many organizations sign in CI keyless, because the identity is exactly "this workflow, on this branch, at this commit" and there's no key to protect, while reserving a KMS-backed key for release artifacts that must be signable outside any CI system entirely, or where a compliance requirement specifically calls for a durable, organization-owned key rather than an individual pipeline's OIDC identity. Notation and Sigstore aren't really competitors so much as two answers that both plug into the same OCI-native signature model — some registries and cloud vendors default to promoting one over the other, which is often the deciding factor in practice more than a pure technical preference. See the tooling landscape for where signing sits among the rest of this course's tools.
Benny the Beaver: Image's built, pushed, signed. cosign sign ran in about four seconds — no key, no password prompt, nothing to remember.
Pip the Hummingbird: I didn't ask if it's signed. I asked who signed it. There's a difference — a signature just proves nobody tampered with it after some identity vouched for it.
Benny: Fine — the release workflow, on main, GitHub's own OIDC token. Nobody typed a password because there wasn't one to type.
Timmy the Turtle: And that's exactly what the admission policy checks before I let the Pod schedule — not "is there a signature," but "does the identity on that signature match the one workflow I actually trust." Anything else gets rejected at the door.
Foxy: What happens in a year when someone asks "did we really ship this exact image on this date"? The certificate that signed it will have expired in about ten minutes.
Pip: The certificate, sure. The Rekor entry doesn't expire. I don't check today's clock against a cert from a year ago — I check whether the log entry's timestamp fell inside the cert's ten-minute window back when it was made. That part is permanent and public.
Rocky the Raccoon: So if I ever got hold of a CI job's OIDC token for those ten minutes, I could get a real cert with your identity on it.
Pip: You could — and it would show up in the public log the instant you tried it. That's exactly why I watch Rekor for our identity signing things we never built.
1. Walk through what happens, in order, between running cosign sign and the private key being discarded. 2. Why doesn't a Fulcio certificate's short validity window make a keyless signature useless a year later — what makes verification still work? 3. What's the single biggest footgun in cosign verify for keyless signatures, and what does current cosign do to prevent it? 4. Where does signature verification actually get enforced so an unsigned image can't run — and what does that enforcement point check that a client-side cosign verify also checks? 5. Give one concrete reason an organization might choose a KMS-backed keyed signature over keyless, despite the extra key-management burden.
Check your answers
- cosign generates a fresh ephemeral keypair in memory; it proves an identity through an OIDC flow (interactive browser login, or an ambient CI-issued token like GitHub Actions'); it sends the ephemeral public key plus the OIDC token to Fulcio as a certificate request; Fulcio validates the token and issues a short-lived X.509 certificate (roughly ten minutes) binding the verified identity to that public key; cosign signs the artifact's digest with the ephemeral private key and then discards that key permanently — it's never written to disk.
- Because verification doesn't check the certificate's validity against today's date — it checks that Rekor's recorded inclusion timestamp for that signing event falls inside the certificate's original short validity window. The certificate only ever had to be valid for the moment it signed; the permanent, tamper-evident Rekor log entry is the durable proof that it was valid then, and that entry doesn't expire.
- Running
cosign verifywithout--certificate-identityand--certificate-oidc-issuer, which — in older versions or older tutorials — would accept a valid signature from any keyless signer at all, not specifically yours. Current cosign requires those identity-pinning flags for keyless verification and refuses to run without them. - At a Kubernetes admission-control webhook — Kyverno's
verifyImagesor Sigstore's ownpolicy-controllervia aClusterImagePolicy— which runs before a Pod is scheduled. It performs the same checks a client-sidecosign verifydoes: it re-derives trust from Fulcio's root and the Rekor inclusion proof, and confirms the certificate's embedded identity matches the policy's allowed subject and issuer, rejecting the Pod if anything fails. - Any reasonable answer along these lines: the organization needs to sign outside of any CI system entirely (e.g. a manual release step), wants a durable signing identity that belongs to the organization rather than to a pipeline's OIDC account, needs to sign and verify fully offline with no dependency on Fulcio/Rekor availability, or has a compliance requirement that specifically calls for a long-lived, custody-tracked key rather than an ephemeral identity-bound certificate.