Signing and Verifying Container Images with Sigstore and Cosign

Introduction: Why Artifact Signing Matters in CI/CD

Modern software delivery pipelines are remarkably good at building and shipping code fast. But speed without trust is a liability. Between the moment source code is committed and the moment a container image runs in production, there is a gap — a gap where tampering, substitution, or silent corruption can occur without anyone noticing.

This is not a theoretical concern. The SolarWinds attack in 2020 demonstrated how adversaries could inject malicious code into a build process, producing signed but compromised artifacts that propagated to thousands of organizations. The Codecov breach in 2021 showed how a tampered CI script could exfiltrate secrets from every repository that used it. In both cases, the supply chain — the infrastructure between code and deployment — was the attack surface.

Artifact signing addresses one critical piece of this puzzle: authenticity and integrity. By cryptographically signing a container image, you create a verifiable link between the image and the identity (person, team, or CI system) that produced it. Consumers of that image can then verify the signature before running it, ensuring it has not been modified since it was built.

For years, signing was impractical in most organizations. GPG key management was cumbersome, key distribution was fragile, and the tooling required deep cryptographic expertise. Sigstore changed that. It introduced a suite of open-source tools that make signing and verification accessible, automated, and — critically — keyless.

This guide walks through the Sigstore ecosystem, shows you how to sign and verify container images with Cosign, integrate signing into CI/CD pipelines, and attach attestations and SBOMs. By the end, you will have a practical understanding of how to make artifact signing a standard part of your delivery process.

What is Sigstore?

Sigstore is an open-source project under the Linux Foundation that provides free, transparent, and easy-to-use tools for signing, verifying, and protecting software artifacts. It was created to solve a specific problem: making cryptographic signing practical for the open-source ecosystem and beyond.

The Sigstore ecosystem consists of three core components:

Cosign

Cosign is the client-side tool for signing and verifying container images and other OCI artifacts. It is what developers and CI/CD pipelines interact with directly. Cosign supports both traditional key-pair signing and the newer keyless signing flow.

Fulcio

Fulcio is a free certificate authority (CA) that issues short-lived certificates based on an OpenID Connect (OIDC) identity. When you use keyless signing, Fulcio verifies your identity through an OIDC provider (such as Google, GitHub, or Microsoft) and issues a certificate that binds your identity to a signing key. The certificate is valid for only a few minutes — long enough to sign an artifact, but short enough that key compromise is not a persistent risk.

Rekor

Rekor is a transparency log — an immutable, append-only ledger that records signing events. Every time an artifact is signed, a record is added to Rekor. This provides a public, auditable trail of who signed what and when. It is conceptually similar to Certificate Transparency logs used in the TLS ecosystem.

How Keyless Signing Differs from GPG

Traditional GPG-based signing requires you to generate a long-lived key pair, protect the private key, distribute the public key, and manage key rotation and revocation. This is operationally heavy and error-prone, which is why most projects never adopted signing at all.

Sigstore’s keyless signing eliminates this burden:

  • No long-lived keys — Signing keys are ephemeral. They exist only for the duration of the signing operation.
  • Identity-based trust — Instead of trusting a key, you trust an identity (e.g., a GitHub Actions workflow, a specific email address). Fulcio binds the identity to the ephemeral key via a short-lived certificate.
  • Transparency by default — Every signing event is logged in Rekor, creating an auditable record without requiring you to run your own infrastructure.
  • No key distribution problem — Verifiers do not need to obtain a public key out-of-band. They verify against the identity and the transparency log.

Signing Container Images with Cosign

Installing Cosign

Cosign is distributed as a single binary. You can install it on most platforms:

# macOS (Homebrew)
brew install cosign

# Linux (Go install)
go install github.com/sigstore/cosign/v2/cmd/cosign@latest

# Linux (binary release)
curl -LO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign

# Verify installation
cosign version

Signing with a Key Pair

The simplest approach is to generate a key pair and use it to sign images. This is useful when you want full control over key management or when keyless signing is not available in your environment.

Step 1: Generate a key pair

cosign generate-key-pair

This creates two files: cosign.key (private key, password-protected) and cosign.pub (public key). Store the private key securely — in a secrets manager, HSM, or encrypted vault.

Step 2: Sign a container image

# Sign an image by its digest (always prefer digest over tag)
cosign sign --key cosign.key ghcr.io/myorg/myapp@sha256:abc123...

Cosign will prompt for the private key password, generate a signature, and push it to the same OCI registry alongside the image. The signature is stored as a separate OCI artifact, tagged with a convention that links it to the image digest.

Important: Always sign by digest, not by tag. Tags are mutable — someone could move a tag to point to a different image after signing. Digests are content-addressed and immutable.

Keyless Signing with OIDC Identity

Keyless signing is the recommended approach for CI/CD pipelines. It removes the need to manage signing keys entirely.

# Keyless signing (interactive — opens browser for OIDC login)
cosign sign ghcr.io/myorg/myapp@sha256:abc123...

# Keyless signing (non-interactive, for CI/CD)
# The --yes flag skips the confirmation prompt
cosign sign --yes ghcr.io/myorg/myapp@sha256:abc123...

In a CI/CD environment like GitHub Actions, Cosign automatically detects the ambient OIDC token provided by the platform. No browser interaction is needed.

What Happens Behind the Scenes

When you run cosign sign --yes in keyless mode, the following sequence occurs:

  1. Ephemeral key generation — Cosign generates a temporary key pair in memory.
  2. OIDC authentication — Cosign obtains an OIDC identity token from the environment (e.g., GitHub Actions OIDC provider) or prompts you to authenticate via a browser.
  3. Certificate issuance — Cosign sends the public key and the OIDC token to Fulcio. Fulcio verifies the token, extracts the identity (email, workflow URI, etc.), and issues a short-lived X.509 certificate binding the identity to the public key.
  4. Signing — Cosign signs the image digest using the ephemeral private key.
  5. Transparency logging — The signature, certificate, and image digest are recorded in Rekor. This entry is timestamped and immutable.
  6. Signature upload — The signature is pushed to the OCI registry as a companion artifact.
  7. Key destruction — The ephemeral private key is discarded. It is never stored anywhere.

The result is a signed image with a verifiable chain: the Rekor log proves the signature was created at a specific time by a specific identity, and the Fulcio certificate proves the identity was authenticated at the time of signing.

Verifying Signatures

Signing is only useful if consumers verify. Cosign provides straightforward verification commands for both key-based and keyless scenarios.

Verification with a Public Key

If the image was signed with a key pair, verify using the public key:

cosign verify --key cosign.pub ghcr.io/myorg/myapp@sha256:abc123...

Cosign fetches the signature from the OCI registry, verifies it against the public key, and outputs the result. If the signature is valid, it prints the signing payload. If not, it exits with an error.

Keyless Verification

For keyless-signed images, verification is based on identity rather than a key. You specify who you expect to have signed the image:

# Verify that a specific GitHub Actions workflow signed the image
cosign verify \
  --certificate-identity "https://github.com/myorg/myapp/.github/workflows/release.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myapp@sha256:abc123...

This command checks that:

  • The image has a valid signature.
  • The signing certificate was issued by Fulcio.
  • The identity in the certificate matches the specified --certificate-identity.
  • The OIDC issuer matches --certificate-oidc-issuer.
  • The signing event is recorded in the Rekor transparency log.

You can also use regex matching for more flexible policies:

cosign verify \
  --certificate-identity-regexp "https://github.com/myorg/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myapp@sha256:abc123...

Enforcing Verification in Admission Controllers

Manual verification is useful for debugging, but production environments need automated enforcement. Kubernetes admission controllers can reject any image that does not have a valid signature.

Kyverno is a popular policy engine with built-in Cosign verification support:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/myorg/myapp/.github/workflows/release.yml@refs/heads/main"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev

The Sigstore Policy Controller (formerly Cosign Policy Webhook) provides similar functionality with a Sigstore-native approach:

apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: require-signed-images
spec:
  images:
    - glob: "ghcr.io/myorg/**"
  authorities:
    - keyless:
        identities:
          - issuer: "https://token.actions.githubusercontent.com"
            subject: "https://github.com/myorg/myapp/.github/workflows/release.yml@refs/heads/main"
        ctlog:
          url: https://rekor.sigstore.dev

With either approach, any pod that references an unsigned (or incorrectly signed) image from your registry will be rejected at admission time.

Integrating Signing into CI/CD Pipelines

GitHub Actions with Keyless Signing

GitHub Actions has native OIDC support, making it the ideal environment for keyless signing. Here is a complete workflow that builds, pushes, and signs a container image:

name: Build 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:
      - name: Checkout code
        uses: actions/checkout@v4

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

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

      - name: Build and push image
        id: build
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

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

      - name: Sign the image
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: |
          cosign sign --yes \
            ghcr.io/${{ github.repository }}@${DIGEST}

Key points:

  • The id-token: write permission enables the GitHub Actions OIDC provider, which Cosign uses for keyless signing.
  • The image is signed by digest, not by tag, using the output from the build step.
  • No secrets or keys are needed — the OIDC token from GitHub is the identity.

GitLab CI Example

GitLab CI also supports OIDC tokens for keyless signing. The approach is similar but uses GitLab’s ID token mechanism:

stages:
  - build
  - sign

variables:
  IMAGE: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}

build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $IMAGE .
    - docker push $IMAGE
    - echo "DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $IMAGE | cut -d@ -f2)" >> build.env
  artifacts:
    reports:
      dotenv: build.env

sign:
  stage: sign
  image: alpine:3.19
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  before_script:
    - apk add --no-cache cosign
  script:
    - cosign sign --yes ${CI_REGISTRY_IMAGE}@${DIGEST}

The id_tokens block instructs GitLab to generate an OIDC token with the audience sigstore. Cosign picks up the token from the SIGSTORE_ID_TOKEN environment variable automatically.

Storing Signatures in OCI Registries

By default, Cosign stores signatures in the same OCI registry as the signed image. The signature is pushed as a separate tag following the convention sha256-<digest>.sig. This means:

  • No additional infrastructure is needed for signature storage.
  • Signatures travel with the image when mirrored or replicated.
  • Most major registries (GHCR, Docker Hub, ECR, GCR, ACR, Harbor) support OCI artifacts and work with Cosign out of the box.

If you need to store signatures in a different registry (for example, a dedicated signature store), you can use the COSIGN_REPOSITORY environment variable:

export COSIGN_REPOSITORY=ghcr.io/myorg/signatures
cosign sign --yes ghcr.io/myorg/myapp@sha256:abc123...

Attestations and SBOM Attachment

Signatures prove who built an image. Attestations go further — they prove how it was built and what it contains. Cosign supports attaching structured metadata to images in the form of in-toto attestations.

Attaching SLSA Provenance with cosign attest

SLSA (Supply-chain Levels for Software Artifacts) provenance describes the build process: what source was used, what builder ran, what steps were executed. You can attach a SLSA provenance attestation to an image:

# Create a provenance statement (simplified example)
cat > provenance.json <<'EOF'
{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "ghcr.io/myorg/myapp",
      "digest": {
        "sha256": "abc123..."
      }
    }
  ],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "buildType": "https://github.com/myorg/myapp/.github/workflows/release.yml",
      "resolvedDependencies": [
        {
          "uri": "git+https://github.com/myorg/myapp@refs/heads/main",
          "digest": {
            "sha1": "def456..."
          }
        }
      ]
    },
    "runDetails": {
      "builder": {
        "id": "https://github.com/actions/runner"
      }
    }
  }
}
EOF

# Attest the image with the provenance statement (keyless)
cosign attest --yes \
  --predicate provenance.json \
  --type slsaprovenance \
  ghcr.io/myorg/myapp@sha256:abc123...

In practice, you would use a SLSA generator (such as slsa-github-generator) to produce the provenance automatically rather than crafting it by hand.

Attaching SBOMs

A Software Bill of Materials (SBOM) lists every component inside your container image. Cosign can attach an SBOM to an image so that consumers can inspect its contents:

# Generate an SBOM using Syft
syft ghcr.io/myorg/myapp@sha256:abc123... -o spdx-json > sbom.spdx.json

# Attach the SBOM as an attestation (recommended approach)
cosign attest --yes \
  --predicate sbom.spdx.json \
  --type spdxjson \
  ghcr.io/myorg/myapp@sha256:abc123...

Alternatively, you can use the older cosign attach sbom command, though the attestation-based approach is preferred because it is signed and verifiable:

# Older approach (attached but not signed)
cosign attach sbom --sbom sbom.spdx.json \
  ghcr.io/myorg/myapp@sha256:abc123...

Verifying Attestations

Consumers can verify attestations just like signatures. The cosign verify-attestation command checks both the signature on the attestation and the identity of the signer:

# Verify SLSA provenance attestation
cosign verify-attestation \
  --type slsaprovenance \
  --certificate-identity "https://github.com/myorg/myapp/.github/workflows/release.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myapp@sha256:abc123...

# Verify SBOM attestation
cosign verify-attestation \
  --type spdxjson \
  --certificate-identity "https://github.com/myorg/myapp/.github/workflows/release.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myapp@sha256:abc123...

You can also use policy engines like Kyverno to verify attestations at admission time, ensuring that only images with valid provenance or SBOMs are deployed to your clusters.

Security Considerations and Trade-offs

Artifact signing is a powerful tool, but it is important to understand what it does and does not protect against.

What Signing Protects Against

  • Tampering after build — If someone modifies an image after it has been signed, the signature will no longer verify. This catches registry compromises, man-in-the-middle attacks, and accidental corruption.
  • Impersonation — With keyless signing, the identity of the signer is cryptographically bound to the signature. An attacker cannot forge a signature that claims to come from your CI pipeline without compromising the OIDC provider.
  • Repudiation — The Rekor transparency log provides a tamper-evident record of signing events. Signers cannot deny having signed an artifact.

What Signing Does NOT Protect Against

  • Malicious source code — Signing proves the image was built by an authorized pipeline. It does not prove the source code is free of vulnerabilities or backdoors. A compromised developer account that pushes malicious code will produce a legitimately signed image.
  • Compromised build environments — If the CI runner itself is compromised (as in the SolarWinds scenario), the attacker can produce signed artifacts. Signing proves identity, not integrity of the build environment.
  • Vulnerabilities in dependencies — A signed image can still contain known CVEs. Signing and vulnerability scanning are complementary, not substitutes.
  • Policy bypass — Signing only works if verification is enforced. If your admission controller has exceptions, or if teams can deploy without going through it, signing provides no protection for those paths.

Trust Model Assumptions

Keyless signing relies on several trust assumptions:

  • Trust in the OIDC provider — You trust that GitHub, GitLab, or Google correctly authenticates identities. A compromise of the OIDC provider undermines the entire model.
  • Trust in Fulcio — You trust the Sigstore Fulcio instance to correctly verify OIDC tokens and issue certificates only to authenticated identities.
  • Trust in Rekor — You trust the transparency log to be append-only and tamper-evident. Sigstore’s public Rekor instance is operated by the community; for high-assurance environments, you may want to run your own.

For organizations with strict compliance requirements, running a private Sigstore infrastructure (private Fulcio CA, private Rekor log) provides full control over the trust chain.

Key Management Considerations

If you choose key-based signing instead of keyless:

  • Store private keys in a KMS (AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault). Cosign has native KMS support: cosign sign --key awskms:///arn:aws:kms:...
  • Rotate keys on a regular schedule and have a revocation plan.
  • Avoid storing private keys as CI/CD secrets — they are typically logged, cached, and replicated in ways that increase exposure.
  • Use separate keys for different environments (staging vs. production) to limit blast radius.

Conclusion

Container image signing with Sigstore and Cosign is one of the most impactful steps you can take toward securing your software supply chain. It is no longer difficult, no longer requires deep cryptographic expertise, and no longer demands complex key management infrastructure. With keyless signing, a CI/CD pipeline can sign every image it produces with zero secrets to manage and full auditability through the Rekor transparency log.

But signing is a building block, not a silver bullet. It answers the question “was this image produced by an authorized process?” It does not answer “is this image safe to run?” A complete supply chain security strategy combines signing with vulnerability scanning, SBOM generation, SLSA provenance, policy enforcement at admission, runtime security monitoring, and rigorous access controls on source repositories and build infrastructure.

Start by adding cosign sign --yes to your CI/CD pipeline. Then add verification in your admission controller. Then layer on attestations and SBOMs. Each step narrows the gap between building software and trusting it.

The tools are mature, the ecosystem is growing, and the cost of not signing is becoming harder to justify. The question is no longer whether to sign your artifacts — it is how quickly you can make it the default.