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:
- Rotation is tedious. Every 90 days (or your org’s policy), you generate a new key, update every CI system that references it, verify the old one still works for verification, then retire it. Each step is error-prone.
- Storage is a trust game. Is your secret manager secure? Is the secret manager’s secret secure? How many humans can access it? What if the person who set it up leaves?
- Compromise is a nightmare. A leaked signing key means someone can sign anything with authority. You can revoke it, but you don’t know what they signed yesterday. Every image signed with that key is suddenly suspect.
- Delegation is painful. Want different teams to sign different images? You end up managing separate keys and access tiers, which multiplies the problem.
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:
name: build-and-signon: [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:latestThat 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:
- The workflow’s repo and ref
- The GitHub Actions service account identity
- The timestamp and expiry (usually 5–15 minutes)
- A signature from GitHub’s private key
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:
- Valid for ~5–10 minutes
- Bound to the issuer (
https://token.actions.githubusercontent.com) and the subject (your OIDC identity) - Contains a freshly generated private key—which only cosign (and Fulcio in that moment) knows about
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:
- The signature is cryptographically valid for the image hash
- The signature was issued by a trusted identity
With keyless signing, verification is straightforward:
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:latestLet’s break this down:
--certificate-identity: The exact identity that signed the image. This is the OIDC subject claim. For GitHub Actions, it follows the patternhttps://github.com/{owner}/{repo}/.github/workflows/{workflow}@refs/heads/{branch}or@refs/tags/{tag}.--certificate-oidc-issuer: The OIDC provider’s URL. For GitHub, it’s alwayshttps://token.actions.githubusercontent.com.
cosign then:
- Fetches the image manifest and signature from your registry
- Downloads the signing certificate from Rekor (or it’s embedded in the signature)
- Verifies the certificate signature against GitHub’s public OIDC keys
- Verifies the image signature against the certificate’s public key
- Verifies the certificate’s identity claims match your flags
- 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:
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: 1A few things to note:
id-token: writeis mandatory. Without it, your workflow can’t get an OIDC token.COSIGN_EXPERIMENTAL: 1is an environment variable that tells cosign to use keyless mode (it’s been stable for years, so the name is misleading—legacy naming).cosign sign --yesdoesn’t prompt for confirmation. Use this in CI; remove--yesfor manual signing on your workstation.- You’re signing the image by
@digest(its canonical hash), not by tag, because tags are mutable. This is cryptographic hygiene.
After this runs, your image is signed, the signature is in Rekor, and you can verify it with:
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:latestKubernetes 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:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-signed-imagesspec: 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:
- You’re deploying from GitHub Actions (or GitLab, or another cloud CI with OIDC support).
- You want zero secret management overhead.
- You’re running a team and want to scale identity without distributing keys.
- You care about supply chain security and audit trails.
Stick with GPG / cosign-with-key when:
- You’re signing on your workstation for a release (no OIDC token available).
- You need to sign artifacts outside of CI (e.g., manually approving a deploy).
- You’re using a CI provider without OIDC support (hire someone to add it, honestly).
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:
- No key rotation. No key leaks. No secret management infrastructure.
- Every signature is logged publicly in Rekor (tamper-proof audit trail).
- Identity is tied to your CI provider’s trust domain (GitHub can revoke it).
- Ephemeral keys are issued on-demand and discarded immediately.
What you lose (minimal):
- A hard dependency on Fulcio and Rekor being available during signing. If both go down, you can’t sign (but you can still build and push; signing just fails). This is rare—Sigstore is run by the Linux Foundation.
- A slight initial learning curve understanding OIDC, Fulcio, and Rekor (you’re reading this, so you’ve got it).
- The ability to sign images in a completely offline environment (you need to reach Fulcio and Rekor).
The trade-offs are phenomenally one-sided in favor of keyless.
Next Steps: Make It Real
- Add cosign to your workflow. Use
sigstore/cosign-installer@v3andid-token: writein permissions. - Sign one image. Run
cosign sign --yes $IMAGEand watch the magic happen. - Verify it. Query
cosign verify --certificate-identity ... $IMAGEand see the certificate chain. - Check Rekor. Visit https://rekor.sigstore.dev, search by image digest, and see your signature logged publicly.
- 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
- Sigstore Docs: https://docs.sigstore.dev (official, comprehensive)
- Cosign GitHub: https://github.com/sigstore/cosign (the tool itself)
- Rekor Browser: https://rekor.sigstore.dev (see every signature logged)
- OpenID Connect in GitHub Actions: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect (the trust foundation)