Skip to content
Go back

Cosign Keyless: Sign Without Keys

By SumGuy 12 min read
Cosign Keyless: Sign Without Keys

You’re Managing Private Keys to Sign Containers. Stop.

Here’s the thing: every container you push without a signature is a security debt. Someone could swap it with malware, and your deployment pipeline would happily pull it down. So you sign. Good instinct.

But then you have to manage the key. Rotate it every 90 days. Store it somewhere “secure.” Hope nobody accidentally commits it to git. Restore it during deploys. Worry about it 3 AM when the key management system is down.

There’s a better way. Cosign keyless signing lets you sign container images using ephemeral certificates tied to your GitHub Actions token—no long-lived private key file required. You run cosign sign --yes ... in your workflow, and magic happens: GitHub’s OIDC provider vouches for you, a temporary signing cert gets issued, you sign the image, and the signature lands in a public transparency log. No key to leak. No rotation dance. Just cryptographic proof that your CI signed it.

This is Sigstore—a transparency-first signing infrastructure built by the same people who maintain TUF and in-toto. And yes, it’s boring enough that you’ve probably heard of it and dismissed it. Let me convince you otherwise.


The Problem With Long-Lived Signing Keys

Every key is a liability. You can encrypt it, sure, but encryption is the security theater of key management. The real problem is entropy:

Traditional approaches try to patch this: GPG subkeys (overly complex), hardware security modules (expensive and operational burden), KMS providers (adds latency and failure modes). All of them still boil down to: you have a long-lived secret somewhere.

Keyless signing inverts this. Instead of “prove you have the secret key,” it’s “prove your identity through a protocol we trust, and we’ll issue you a temporary key just for this signature.”


Meet Sigstore: OIDC + Fulcio + Rekor

Sigstore is a three-piece band: an OIDC provider (yours—in this case, GitHub), a CA that issues short-lived certs (Fulcio), and a public ledger that logs every signature (Rekor).

OIDC provider (GitHub Actions): When your workflow runs, GitHub’s OIDC provider issues a token that says “this is GitHub Actions workflow XYZ on repo ABC branch main, running as user kingpin, at 2026-05-29T12:00:00Z.” The token is cryptographically signed by GitHub and expires in 5-15 minutes.

Fulcio (the CA): Your cosign tool sends this OIDC token to Fulcio—a public certificate authority run by the Sigstore project. Fulcio verifies the token signature (proving GitHub issued it), then issues an ephemeral X.509 certificate. This cert is valid for only 5-10 minutes and is bound to your OIDC identity (e.g., https://github.com/yourorg). You never see the private key—Fulcio generates it and immediately returns it to your cosign process.

Rekor (the transparency log): After you sign the image, cosign posts the signature + the certificate to Rekor—a publicly auditable ledger. Anyone can query Rekor by artifact hash and see who signed it and when. This is the transparency part: you can audit the entire signature history of the internet if you want (or your org, or your registry, depending on policy).

The result: your GitHub Actions runner signs images without ever storing a private key. The signature is verifiable using only the OIDC issuer and Rekor.


The Keyless Flow in Action

Let’s walk through what happens when you run cosign in GitHub Actions with OIDC.

1. Workflow starts:

ci.yaml
name: build-and-sign
on: [push]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Critical: allows cosign to request OIDC token
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: sigstore/cosign-installer@v3
- name: Build and sign container
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
run: |
docker build -t $REGISTRY/$IMAGE_NAME:latest .
docker push $REGISTRY/$IMAGE_NAME:latest
cosign sign --yes $REGISTRY/$IMAGE_NAME:latest

That id-token: write permission is not optional—it’s how GitHub’s OIDC provider knows to issue a token to this workflow.

2. cosign requests a token:

When you run cosign sign, it first asks GitHub’s OIDC provider for a token. GitHub generates one that includes:

3. Fulcio issues a certificate:

cosign sends this token to Fulcio. Fulcio’s OpenID Connect validator verifies the signature and extracts the issuer and subject. It then issues an ephemeral X.509 certificate:

4. cosign signs the image:

cosign uses the ephemeral private key to sign the image hash with ECDSA-P256. The signature is short and deterministic.

5. Rekor records it:

cosign posts the signature + certificate + OIDC token to Rekor. Rekor verifies the signature chain, then appends an entry to its immutable ledger. This entry is assigned a unique ID (UUID) that you can share as proof of signing.

6. Certificate is discarded:

The ephemeral key is never written to disk. After signing, it’s gone. Even if someone compromises the Actions runner, there’s no key to steal.


Verification: The Other Side of the Coin

Signing is half the story. The other half is verification: proving that an image was signed by your CI, not by a bad actor.

When someone (or a Kubernetes admission controller) verifies a signature, they need to prove two things:

  1. The signature is cryptographically valid for the image hash
  2. The signature was issued by a trusted identity

With keyless signing, verification is straightforward:

Terminal window
cosign verify \
--certificate-identity 'https://github.com/yourorg/yourrepo/.github/workflows/ci.yaml@refs/heads/main' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
ghcr.io/yourorg/yourrepo:latest

Let’s break this down:

cosign then:

  1. Fetches the image manifest and signature from your registry
  2. Downloads the signing certificate from Rekor (or it’s embedded in the signature)
  3. Verifies the certificate signature against GitHub’s public OIDC keys
  4. Verifies the image signature against the certificate’s public key
  5. Verifies the certificate’s identity claims match your flags
  6. Returns a success or failure

The beautiful part: you never distribute, rotate, or manage a signing key. You just trust GitHub’s OIDC and Sigstore’s infrastructure.


Integrating Into CI/CD: A Complete Example

Here’s a real workflow that builds a multi-arch image, signs it keyless, and pushes to GHCR:

.github/workflows/publish.yaml
name: Publish
on:
push:
branches: [main]
pull_request:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: sigstore/cosign-installer@v3
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha
- uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Sign container image
run: |
cosign sign --yes "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
env:
COSIGN_EXPERIMENTAL: 1

A few things to note:

After this runs, your image is signed, the signature is in Rekor, and you can verify it with:

Terminal window
cosign verify \
--certificate-identity "https://github.com/yourorg/yourrepo/.github/workflows/publish.yaml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/yourorg/yourrepo:latest

Kubernetes Admission Control: Enforcing Verification

The real power shows up when you enforce signature verification at deployment time. You can use policy engines like Kyverno or Kubewarden to reject any image that isn’t signed by your OIDC identity.

Here’s a Kyverno policy that requires images to be signed:

kyverno-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: enforce
webhookTimeoutSeconds: 30
rules:
- name: check-image-signature
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/yourorg/*"
attestations:
- name: verify-signature
attestationScript: |
cosign verify \
--certificate-identity "https://github.com/yourorg/*/.github/workflows/*.yaml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
{{ image }}

Now, any Pod that tries to run an unsigned image (or signed by a different identity) gets blocked. Your cluster enforces that only your CI can deploy.


Common Pitfalls and How to Avoid Them

Rekor is down (it happens). Rekor uptime is excellent, but if it’s unavailable, cosign sign will hang by default. Set --insecure-skip-tlog-verify=false on signing (it’s the default) and understand that temporarily you might not be logging to the ledger. This is a minor inconvenience, not a security issue—the signature is still valid.

Identity verification mistakes. The --certificate-identity flag is a regex. If you get it wrong, verification fails silently. Test it once, then copy-paste everywhere. A common mistake: forgetting that the identity includes the full workflow path (/.github/workflows/build.yaml@refs/heads/main), not just the repo.

Using the wrong OIDC issuer. GitHub Actions is https://token.actions.githubusercontent.com. GitLab is https://gitlab.com. Make sure you match your CI provider. If you’re using a self-hosted runner, you might need to configure a custom OIDC provider—this is advanced and beyond scope here, but it’s possible.

Signing on your workstation vs CI. On your local machine, you don’t have an OIDC token. You’d need to use traditional cosign with a key file (or use cosign generate-key-pair and manage it locally). Don’t mix the two—sign in CI with keyless, sign locally with a key. This avoids the situation where you deploy unsigned code because the OIDC server was temporarily unavailable.

Forgetting id-token: write. This is the number-one reason keyless signing fails in new workflows. If you get “OIDC token not found,” check your permissions.


Keyless vs. Traditional Signing: Which One?

Use keyless signing when:

Stick with GPG / cosign-with-key when:

The honest take: there’s no reason to use long-lived signing keys in 2026 if your CI supports OIDC. Keyless signing is cheaper, safer, and more auditable.


The Trade-Offs You Should Know

What you gain:

What you lose (minimal):

The trade-offs are phenomenally one-sided in favor of keyless.


Next Steps: Make It Real

  1. Add cosign to your workflow. Use sigstore/cosign-installer@v3 and id-token: write in permissions.
  2. Sign one image. Run cosign sign --yes $IMAGE and watch the magic happen.
  3. Verify it. Query cosign verify --certificate-identity ... $IMAGE and see the certificate chain.
  4. Check Rekor. Visit https://rekor.sigstore.dev, search by image digest, and see your signature logged publicly.
  5. Enforce it. Deploy Kyverno or similar to block unsigned images in your cluster.

After a week, you’ll forget you ever worried about signing keys. That’s the goal.

Keyless signing is infrastructure that gets out of your way. Set it up once, forget it exists, sleep better knowing your supply chain is auditable and your secrets are ephemeral.


Further Reading


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Sentry Self-Hosted for Application Errors
Next Post
Docker Bake vs Compose Build

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts