Pavan Rangani

HomeBlogContainer Image Signing with Sigstore Cosign: Zero-Trust Supply Chain Security

Container Image Signing with Sigstore Cosign: Zero-Trust Supply Chain Security

By Pavan Rangani · March 20, 2026 · Security

Container Image Signing with Sigstore Cosign: Zero-Trust Supply Chain Security

Container Image Signing with Sigstore Cosign

Container image signing Cosign has become the industry standard for verifying that container images running in production are exactly what your CI/CD pipeline built. Sigstore’s Cosign tool provides cryptographic signatures that prove image provenance, detect tampering, and enforce trust policies at deployment time. With keyless signing through OIDC identity, there are no private keys to manage or rotate — a property that removes one of the most error-prone parts of traditional code signing.

This guide covers the complete signing workflow: from signing images in CI pipelines to verifying signatures with Kubernetes admission controllers. You will learn how to implement a zero-trust container supply chain that prevents unauthorized or modified images from ever reaching your clusters, and where the approach is and is not worth the operational cost.

Understanding Sigstore and Cosign

Sigstore is an open-source project that provides free code signing infrastructure. Cosign, its container signing tool, uses three components: a transparency log (Rekor) that records all signing events, a certificate authority (Fulcio) that issues short-lived certificates, and OIDC identity providers for keyless signing. Crucially, signatures are stored as separate OCI artifacts in the same registry as the image, addressed by the image digest — so the signature travels with the image wherever it is pulled.

Container image signing security architecture
Sigstore ecosystem: Cosign, Fulcio CA, and Rekor transparency log working together
Keyless Signing Flow:

1. Developer/CI triggers cosign sign
2. Cosign requests OIDC token (GitHub, Google, etc.)
3. Fulcio CA verifies OIDC token, issues short-lived cert
4. Cosign signs the image digest with the cert
5. Signature + cert stored in OCI registry alongside image
6. Signing event recorded in Rekor transparency log

Verification Flow:
1. Kubernetes admission controller intercepts pod creation
2. Policy engine fetches signature from OCI registry
3. Verifies signature against Fulcio root CA
4. Checks Rekor log for signing event
5. Validates identity (who signed) and claims
6. Admits or rejects the pod

Why Keyless Signing Changes the Threat Model

The traditional objection to code signing is key management: a long-lived private key is a high-value secret that must be stored, rotated, and protected from exfiltration. If it leaks, every signature it ever produced becomes suspect. Keyless signing sidesteps this entirely. Fulcio issues a certificate that is valid for only a few minutes — long enough to sign one image and then expire. There is no standing secret to steal.

Instead of “do you hold the private key?”, verification asks “was this signed by the identity I expect, recorded in a public log?” That identity is the OIDC subject — a GitHub Actions workflow, a Google account, an SPIFFE ID. Because Rekor is an append-only transparency log, an attacker cannot quietly backdate or hide a signing event. As a result, the security question shifts from secret custody to identity and provenance, which most teams already reason about through their CI configuration. This pairs naturally with broader supply chain security SBOM and SLSA guidance, where signed provenance is one pillar among several.

Signing Images in CI/CD Pipelines

The most common approach is keyless signing in GitHub Actions, where the OIDC token from the workflow identity automatically authenticates with Fulcio. No secrets or keys are needed. Note the critical detail of signing by digest, never by tag: tags are mutable and can be repointed to a different image after signing, while a digest is a content-addressed hash that cannot be forged.

# .github/workflows/build-sign.yml
name: Build, Push, and Sign Container Image
on:
  push:
    branches: [main]

permissions:
  contents: read
  packages: write
  id-token: write  # Required for keyless signing

jobs:
  build-sign:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push
        id: build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/myorg/myapp:latest,ghcr.io/myorg/myapp:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Install Cosign
        uses: sigstore/cosign-installer@v3

      - name: Sign Image (Keyless)
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: |
          cosign sign --yes \
            ghcr.io/myorg/myapp@${DIGEST}

      - name: Attach SBOM
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: |
          # Generate SBOM with syft
          syft ghcr.io/myorg/myapp@${DIGEST} -o spdx-json > sbom.spdx.json
          # Attach SBOM as signed attestation
          cosign attest --yes \
            --predicate sbom.spdx.json \
            --type spdxjson \
            ghcr.io/myorg/myapp@${DIGEST}

      - name: Verify Signature
        run: |
          cosign verify \
            --certificate-identity-regexp "https://github.com/myorg/.*" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
            ghcr.io/myorg/myapp@${DIGEST} | jq .

Signatures, Attestations, and SBOMs: Knowing the Difference

Teams often conflate these three artifacts, but they answer different questions. A signature answers “who built this image?” An attestation is signed metadata that answers “what is true about this image?” — for example, the SBOM attestation in the pipeline above asserts the exact set of packages and versions baked into the image. A bare SBOM on its own answers “what is inside?” but, unsigned, carries no proof of authorship.

The power comes from combining them. With a signed SBOM attestation, an admission controller can do more than check authorship; it can refuse images whose SBOM contains a banned package or a known-vulnerable version. Consequently, the SBOM stops being a compliance checkbox and becomes an enforceable policy input at deploy time.

# Verify the signed SBOM attestation, not just the signature
cosign verify-attestation \
  --type spdxjson \
  --certificate-identity-regexp "https://github.com/myorg/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myapp@sha256:abc123... | jq '.payload | @base64d | fromjson'

Kubernetes Admission Control with Kyverno

Moreover, signing images is only half the story. You need admission controllers that reject unsigned or incorrectly signed images at the moment a pod is created. Kyverno and Sigstore Policy Controller both integrate with Cosign verification, and both run as validating webhooks so that no unverified workload ever schedules.

# Kyverno policy to enforce Cosign signatures
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-cosign-signature
spec:
  validationFailureAction: Enforce
  background: false
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-image-signature
      match:
        any:
          - resources:
              kinds: ["Pod"]
              namespaces: ["production", "staging"]
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/myorg/*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev
          attestations:
            - type: https://spdx.dev/Document
              conditions:
                - all:
                    - key: "{{ creationInfo.created }}"
                      operator: NotEquals
                      value: ""
    - name: block-unsigned-images
      match:
        any:
          - resources:
              kinds: ["Pod"]
              namespaces: ["production"]
      validate:
        message: "All images must be signed with Cosign"
        deny:
          conditions:
            any:
              - key: "{{ request.object.spec.containers[].image }}"
                operator: AnyNotIn
                value: "ghcr.io/myorg/*"

A subtle but important detail: the subject and issuer in the policy are what actually provide security. Verifying that an image is “signed” without pinning who signed it is nearly worthless — anyone can sign any public image keylessly. The identity constraint (https://github.com/myorg/* issued by GitHub’s OIDC) is what ties the signature back to your trusted build pipeline.

Kubernetes security and admission control
Enforcing container image verification with Kubernetes admission policies

Key-Based Signing for Air-Gapped Environments

Keyless signing depends on reaching Fulcio and Rekor over the internet, which is a non-starter in disconnected networks. For these cases, Cosign supports traditional key pairs. The trade-off returns: you are now responsible for protecting the private key, ideally in a KMS or HSM rather than a file on disk.

# Generate a key pair for environments without internet
cosign generate-key-pair

# Sign with private key
cosign sign --key cosign.key \
  --annotations "build-id=abc123" \
  --annotations "git-sha=def456" \
  registry.internal.com/myapp:v1.0.0

# Verify with public key
cosign verify --key cosign.pub \
  registry.internal.com/myapp:v1.0.0

# Distribute public key as Kubernetes secret
kubectl create secret generic cosign-pub \
  --from-file=cosign.pub=cosign.pub \
  -n kyverno

Additionally, for air-gapped environments you can run a private Rekor instance and a private Fulcio CA internally. This recovers the same transparency and short-lived-certificate guarantees of keyless signing without any external network dependency — at the cost of operating that infrastructure yourself.

Common Failure Modes and How to Debug Them

In practice, most Cosign rollouts stumble on the same few issues. The most frequent is the OIDC permission: forgetting id-token: write in the workflow causes keyless signing to fail with an authentication error, because the runner cannot mint an OIDC token. Another classic is signing a tag in CI but verifying a digest at admission time (or vice versa) — the references must match, which is one more reason to standardize on digests everywhere.

# When verification fails, inspect what Cosign actually finds
# List signatures and attestations attached to the digest
cosign tree ghcr.io/myorg/myapp@sha256:abc123...

# A common error and its cause:
#   "no matching signatures"
#   -> the --certificate-identity / --issuer do not match the signer
#   -> or the image was re-pushed (new digest) but not re-signed

Registry support is another sharp edge. Some older or restrictive registries reject the OCI referrers/tag scheme Cosign uses to store signatures; verify your registry supports the OCI 1.1 referrers API or Cosign’s tag-based fallback before standardizing on it.

When NOT to Use Container Image Signing

Image signing adds complexity to your CI/CD pipeline and deployment workflow. For development environments, personal projects, or internal tooling with low security requirements, the overhead may not be justified. Furthermore, if your images are built and consumed entirely within a single trusted environment — same CI system, same cluster, no external pulls — network-level controls and registry access policies may already provide sufficient assurance.

There are also honest costs to weigh. Admission control in Enforce mode is a hard dependency in the critical path of every deployment: if Sigstore’s public infrastructure is unreachable and you have not configured caching or a private trust root, pods can fail to schedule. Verification adds latency to admission, and a misconfigured identity regex can silently admit images you meant to block. Consequently, implement signing for production workloads, regulated environments, and any pipeline where images cross trust boundaries — and start in Audit mode so you can see what a policy would reject before it blocks real deployments.

DevSecOps supply chain security workflow
Building a zero-trust container supply chain from CI to production

Key Takeaways

Signing with Sigstore provides a practical, free, and industry-standard approach to container supply chain security. Keyless signing eliminates key management overhead, transparency logs provide auditability, and Kubernetes admission controllers enforce trust policies. Start with keyless signing in GitHub Actions, add Kyverno policies for production namespaces, and attach SBOMs for complete supply chain visibility.

  • Always sign by digest, never by mutable tag — and pin signer identity, not merely “is signed”
  • Prefer keyless signing to avoid long-lived private keys; fall back to key pairs only when air-gapped
  • Combine signatures, signed attestations, and SBOMs so policy can enforce on image contents
  • Roll out admission policies in audit mode first, then switch to enforce once verified
  • Document trust roots and identities so future team members can reason about the policy

For more security topics, explore our guide on Kubernetes security best practices and DevSecOps pipeline security. The Sigstore documentation and Cosign GitHub repository are the authoritative references.

In conclusion, Container image signing Cosign turns “trust me, this image is fine” into a verifiable, auditable claim enforced at the cluster boundary. By signing image digests with keyless OIDC identity, attaching signed SBOMs, and gating deployment behind identity-pinned admission policies, you build a supply chain where only images your pipeline produced can ever run. Start with the fundamentals, validate the workflow in audit mode, and continuously measure results to ensure you are getting the most value from these approaches.

← Back to all articles