Container Image Signing with cosign and Sigstore in 2024
TL;DR — cosign 2.4 plus Sigstore keyless signing is the right default for new projects in 2024. Pair it with admission-time verification, or you’ve just added a moot artifact to your build output.
Supply chain attacks against container images aren’t theoretical anymore. The defensive baseline has shifted, and “we trust our registry” is no longer a complete answer. cosign 2.4 paired with Sigstore makes signing and verifying images approachable enough that you can stop putting it off.
I’ve rolled this out for a few different teams now. The first time took weeks because I didn’t understand the keyless flow and tried to manage signing keys manually. The most recent time took an afternoon, because I got out of the way and used the defaults. This post is what I wish I’d known earlier.
The audience is teams that build their own images and deploy to Kubernetes. The same patterns apply to other runtimes, but the admission-time verification example will be K8s-specific.
Why Keyless, In One Paragraph
Traditional code signing requires you to manage a private key. You generate it, you store it, you protect it, you rotate it. You also worry about what happens when an attacker steals it. Sigstore’s keyless flow eliminates the private key: a short-lived signing certificate is issued by Fulcio based on an OIDC identity, the signature and certificate are stored in Rekor (a public transparency log), and verifiers check the certificate, the OIDC claim, and the Rekor entry instead of trusting a long-lived key. You never have a secret to lose. For CI-driven signing this is straightforwardly better than managing static keys.
If you have hard regulatory constraints that require key escrow or air-gapped signing, the traditional cosign generate-key-pair flow still exists. For everyone else, use keyless.
Sign In CI With OIDC
The shape you want: your CI runs the build, gets an OIDC token from the platform (GitHub Actions, GitLab, Buildkite, etc.), and uses that token to obtain a signing certificate from Sigstore. The signature lives in Rekor; the OCI image gets a signature attached as a sibling artifact in the registry.
# GitHub Actions, building and signing an image
name: build-and-sign
on:
push:
branches: [main]
permissions:
id-token: write # required for keyless cosign
contents: read
packages: write # for ghcr.io
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cosign
uses: sigstore/cosign-installer@v3.7.0
with:
cosign-release: 'v2.4.0'
- name: Login
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker buildx build --push --tag "$IMAGE" .
DIGEST=$(docker buildx imagetools inspect "$IMAGE" \
--format '{{ .Manifest.Digest }}')
echo "image=${IMAGE}@${DIGEST}" >> "$GITHUB_OUTPUT"
- name: Sign image
env:
COSIGN_EXPERIMENTAL: "1"
run: cosign sign --yes "${{ steps.build.outputs.image }}"
A few things to call out. Signing operates on the image digest, not the tag. Tags are mutable; signatures must be tied to immutable content. The action grabs the digest after the push and signs that. The id-token: write permission is what lets the runner request an OIDC token; without it the keyless flow can’t work.
For non-GitHub CI, the principle is the same — get an OIDC token from your identity provider and call cosign sign with it. The Sigstore documentation on OIDC providers lists supported identities.
Verify At Pull Time
Signing without verification is a no-op. The signature has to be checked somewhere before the image runs in production. There are two reasonable places.
Admission Controller In Kubernetes
This is my default. A policy controller — policy-controller from Sigstore, or Kyverno with image verification rules, or Connaisseur — runs as an admission webhook. Every time a Pod is created, the controller checks that the image has a valid signature from an expected identity.
# ClusterImagePolicy using sigstore policy-controller
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: signed-by-our-ci
spec:
images:
- glob: "ghcr.io/our-org/**"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: "^https://github.com/our-org/.*/.github/workflows/build-and-sign\\.yml@refs/heads/main$"
ctlog:
url: https://rekor.sigstore.dev
The subjectRegExp is critical and easy to get wrong. It restricts which workflow identities can sign images that pass policy. The example above accepts only the build-and-sign.yml workflow on the main branch of repos under our-org. If you don’t pin both the workflow path and the branch, an attacker who can run a different workflow in the same org gets to sign images that your cluster will accept.
Enforcement scope matters too. Start in audit mode (policy-controller supports warn mode) for at least a week. You will find unsigned images you didn’t know existed: sidecars, init containers, build tooling. Get them signed or explicitly allowlisted before flipping to enforce.
CLI Verification In Deploy Scripts
For non-Kubernetes deploys, run cosign verify in your deployment pipeline before the deploy proceeds:
cosign verify \
--certificate-identity-regexp '^https://github.com/our-org/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"$IMAGE"
If verification fails, the deploy fails. Don’t be tempted to log a warning and proceed. The verification is the entire point.
Attestations, Not Just Signatures
A signature says “someone with this identity created this image.” An attestation says “this image was produced from this source, with this SBOM, by this builder.” Cosign 2.4 supports both, and attestations carry significantly more information.
# Generate an SLSA-style provenance attestation
cosign attest --yes --predicate provenance.json \
--type slsaprovenance \
"$IMAGE"
# Verify the attestation matches expectations
cosign verify-attestation \
--certificate-identity-regexp '^https://github.com/our-org/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--type slsaprovenance \
"$IMAGE" | jq '.payload | @base64d | fromjson'
The provenance attestation contains the builder, the source repo, the commit SHA, and the workflow that produced the image. Your admission policy can check that the commit referenced in the attestation matches the commit in the image label, which catches a class of replay attacks where someone takes a legitimate signature and applies it to a different image.
I run a separate attestation for SBOMs generated by Syft. Tools like Grype can then verify the SBOM signature before consuming it, which closes the loop on vulnerability scanning trust.
This pairs nicely with the patterns I covered in auto-remediation for cloud security findings — verified attestations are exactly the kind of signal you want to drive automated responses from.
Key Rotation Without Keys
The headline win of keyless signing is that you don’t rotate anything. Certificates expire in minutes. The trust root is the OIDC issuer, which you’re already trusting for CI authentication. Rekor’s transparency log is append-only and public.
That said, there are still things to rotate, in a sense:
- Verification policies. If you rotate your CI workflow path or rename a repository, update the
subjectRegExpin your admission policies. - Pre-signed images in your registry. Periodically re-verify older images, especially before promoting them to production. A signature from a year ago is still valid; an organizational change might mean the identity it was signed under is no longer trusted.
- The Sigstore trust root itself. Sigstore publishes a TUF root that clients use to verify Fulcio and Rekor. Cosign manages this automatically, but if you mirror it for air-gapped environments you need to update it on Sigstore’s schedule.
Gotchas
- Image references must be digests, not tags.
cosign sign nginx:latestdoes technically work, but the signature is tied to whatever digestlatestresolves to at sign time. Always sign by digest. Always verify by digest. Tag-based verification is a foot gun. - Multi-arch images need careful handling. Cosign signs the manifest list by default; individual platform manifests aren’t signed unless you ask. For most workloads the manifest-list signature is enough, but admission controllers that verify per-platform may need the recursive flag.
COSIGN_EXPERIMENTALis no longer needed for keyless in cosign 2.x, but a lot of stale documentation still mentions it. Newer cosign versions assume keyless unless told otherwise.- OIDC identity drift. GitHub Actions occasionally changes the format of OIDC subjects. Pin your
subjectRegExploosely enough to survive minor format changes but tightly enough that arbitrary workflows can’t satisfy it. - Air-gapped Rekor. If you can’t reach the public Rekor, you can run a private Rekor and configure cosign to target it. Don’t pretend Rekor doesn’t exist; the transparency log is a real piece of the security model.
- Cluster bootstrap chicken-and-egg. The policy controller itself needs to be running before it can enforce policies. Its own image must come from somewhere; verify it manually during cluster bootstrap.
- CI cache poisoning. If your build cache is unsigned and an attacker can write to it, they can taint the image before signing. The signature will then certify a poisoned artifact. Treat the build cache as part of the supply chain.
Wrapping Up
Image signing in 2024 is the table-stakes hygiene of container deployments. The tooling has matured to the point where it’s no longer an excuse to skip it. cosign 2.4 with Sigstore keyless flow takes an afternoon to set up and gives you a verifiable record of which CI workflow produced every image you run.
The hard part isn’t the signing. It’s the verification policy: getting the identity regex right, handling the long tail of third-party images you depend on, and being disciplined about audit-then-enforce rollout. Do that work once, document it well, and the result is a deployment story you can defend with a straight face when someone asks how you’d detect a registry compromise.