The Sigstore stack

  • Cosign — the CLI for signing + verifying.
  • Fulcio — a CA that issues short-lived (10-minute) certs bound to OIDC identities.
  • Rekor — the public, append-only transparency log of all signatures.
  • TUF root — the root-of-trust metadata for the whole system; auto-updates.

For OSS projects, the public Sigstore instance is free + open. For enterprises with stricter requirements, run your own Fulcio + Rekor (the components are open source).

Install cosign

curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
sudo chmod +x /usr/local/bin/cosign

# macOS
brew install cosign

# Or via mise (see /tutorials/mise-polyglot-runtime-versions.html)
mise use -g cosign@latest

cosign version

Sign an image with a key (the classic mode)

# Generate a keypair (prompts for password)
cosign generate-key-pair
# cosign.pub + cosign.key

# Sign an image (push the signature to the same registry)
cosign sign --key cosign.key registry.example.com/myorg/myapp:1.0

# Verify
cosign verify --key cosign.pub registry.example.com/myorg/myapp:1.0

The key is what you distribute to verifiers. Same problem as PGP — key compromise is bad. Keyless is the way out.

Keyless signing in CI (the modern mode)

In GitHub Actions:

# .github/workflows/sign.yml
name: Build and sign
on:
  push:
    branches: [main]

permissions:
  id-token: write       # needed for OIDC token
  contents: read
  packages: write       # for GHCR push

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

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

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

      - uses: sigstore/cosign-installer@v3

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

cosign sign --yes in a CI environment with OIDC token uses Sigstore's keyless flow automatically: requests OIDC token, exchanges with Fulcio for a short-lived cert, signs the image, posts to Rekor. No keys to manage.

Verify in production

# Verify the image was signed by a specific GitHub Actions workflow
cosign verify \
    --certificate-identity-regexp="^https://github\.com/myorg/myrepo/\.github/workflows/sign\.yml@" \
    --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
    ghcr.io/myorg/myrepo@sha256:abc...

If the signature is valid and was produced by the matching identity at the matching issuer (and exists in Rekor), the command exits 0. Otherwise, non-zero. Use in any CI / deploy pipeline as a gate.

Verify in Kubernetes (Kyverno policy)

Kyverno (see that tutorial) can enforce signature verification at admission time:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  rules:
    - name: signed-by-our-ci
      match:
        any: [{ resources: { kinds: [Pod] } }]
      verifyImages:
        - imageReferences: ["ghcr.io/myorg/*"]
          attestors:
            - entries:
                - keyless:
                    issuer: "https://token.actions.githubusercontent.com"
                    subject: "https://github.com/myorg/*/.github/workflows/sign.yml@*"
                    rekor:
                      url: "https://rekor.sigstore.dev"

Pods using images from ghcr.io/myorg/* are rejected if the image isn't signed by a GitHub Actions workflow from your org.

SBOMs and attestations

Cosign signs not just images but also attestations — signed assertions about images. Common: SBOM (Software Bill of Materials):

# Generate an SBOM with syft
syft ghcr.io/myorg/myapp:1.0 -o cyclonedx-json > sbom.json

# Attest (attach the signed SBOM to the image)
cosign attest --predicate sbom.json \
    --type cyclonedx \
    ghcr.io/myorg/myapp@sha256:abc...

# Verify the SBOM later
cosign verify-attestation --type cyclonedx \
    --certificate-identity-regexp="..." \
    --certificate-oidc-issuer="..." \
    ghcr.io/myorg/myapp@sha256:abc...

Now every image has a signed bill of materials attached; vulnerability scanners can verify the SBOM came from the build (not a third party) before relying on it.

Signing non-container artifacts (blobs)

# Sign any file
cosign sign-blob --yes my-release.tar.gz --output-signature my-release.tar.gz.sig \
    --output-certificate my-release.tar.gz.crt

# Verify
cosign verify-blob \
    --certificate my-release.tar.gz.crt \
    --signature my-release.tar.gz.sig \
    --certificate-identity-regexp="..." \
    --certificate-oidc-issuer="..." \
    my-release.tar.gz

Useful for release tarballs, signed scripts in shell installers, ML model files.

Pin to a specific signer

The killer feature: you don't pin to a key; you pin to an identity:

# Identity-based attestor in verify
--certificate-identity="https://github.com/myorg/myrepo/.github/workflows/sign.yml@refs/heads/main"

# Or regex
--certificate-identity-regexp="^https://github\.com/(myorg|trustedorg)/"

If an attacker steals a key, signatures from elsewhere don't pass identity verification — they were signed by a different (attacker's) OIDC subject.

For non-GitHub CI

Sigstore supports any OIDC issuer. GitLab CI, Buildkite, Google Cloud Build, AWS CodePipeline, custom OIDC providers all work. The OIDC token claims (sub, iss) become what verifiers pin to.

Private Sigstore (self-hosted)

For air-gapped or compliance-restricted environments, run your own:

  • Fulcio (CA) — deploy via Helm chart
  • Rekor (log) — ditto
  • TUF root — generate via the Sigstore scaffolding tools
  • Tie Fulcio to your internal OIDC IdP (Authentik, see that tutorial)

Public Sigstore is the easier choice unless you have explicit reason for private.

Worth knowing

  • Always pin to digest, not tag. Signatures are tied to image digests; image:tag signatures can be defeated by tag re-push.
  • Rekor's transparency log is the integrity guarantee. Even if Sigstore's CA is compromised, anomalous signing events appear in the log.
  • Short-lived certs mean no revocation needed. Cosign certs are valid for 10 minutes; you can't sign retroactively, and a "stolen" key only works until that cert expires.
  • This is supply-chain integrity, not authorization. Cosign tells you who signed; whether they're allowed to deploy is a separate policy.