Sigstore gives you a zero-secrets, publicly auditable chain of custody for every artifact you ship. This tutorial wires all four Sigstore components — Cosign, Fulcio, Rekor, and policy-controller — into a single GitHub Actions pipeline that signs container images, attests SBOMs, publishes to the public transparency log, and enforces admission policy in Kubernetes. We will use Cosign v2.2.4, the sigstore/cosign-installer@v3.5.0 action, and Sigstore policy-controller 0.9. Prerequisites: a GitHub repo with id-token: write permission, push access to GHCR, and a dev Kubernetes cluster (kind or minikube is fine). Plan for 60 minutes end-to-end.
Which Sigstore components do I need?
You need Cosign as the CLI, Fulcio as the certificate authority, Rekor as the transparency log, and policy-controller as the Kubernetes admission webhook. The public Sigstore instance is free and production-grade — you do not need to self-host anything to get started.
For air-gapped or high-sovereignty environments, the same components can be deployed privately via sigstore/helm-charts, but that is a separate tutorial. Start with the public instance to learn the mental model.
How do I bootstrap identity in CI?
Grant id-token: write to the workflow and let cosign-installer handle the rest. GitHub Actions issues an OIDC token scoped to the workflow, Fulcio validates it, and Cosign gets a 10-minute code-signing certificate.
permissions:
contents: read
id-token: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9af48c2b6d2a2a1e0
with:
cosign-release: 'v2.2.4'
If you forget id-token: write, Cosign prints Error: getting signer: getting key from Fulcio: retrieving cert: interactive mode not supported. That is the canonical "you forgot to grant OIDC" signal.
How do I sign both the image and an SBOM?
Run cosign sign for the image and cosign attest --predicate for the SBOM. Each produces a separate signature that consumers can verify independently.
export IMAGE=ghcr.io/acme/api
docker buildx build --push --tag $IMAGE:${GITHUB_SHA} --metadata-file meta.json .
DIGEST=$(jq -r '."containerimage.digest"' meta.json)
cosign sign --yes $IMAGE@$DIGEST
syft $IMAGE@$DIGEST -o cyclonedx-json > sbom.cdx.json
cosign attest --yes --predicate sbom.cdx.json \
--type cyclonedx $IMAGE@$DIGEST
The --type cyclonedx flag sets the in-toto predicate type to https://cyclonedx.org/bom, which is the MIME type consumers filter on. Mismatched predicate types are a common reason verifiers skip an attestation silently.
How do I inspect the Rekor entry?
Every signature and attestation you publish is recorded in Rekor, the public transparency log. Use cosign tree to list entries for an image, and rekor-cli to fetch full details.
cosign tree ghcr.io/acme/api@$DIGEST
# Signatures for an image tag:
# ghcr.io/acme/api:sha256-....sig
# Attestations for an image tag:
# ghcr.io/acme/api:sha256-....att
rekor-cli get --log-index 89532104 --format json | jq '.Attestation'
Rekor is append-only, so leaked secrets or mis-signed artifacts are discoverable forever. Treat cosign attest --predicate inputs as public — do not embed credentials in a predicate.
How do I verify in a downstream consumer?
Call cosign verify with the expected certificate identity and OIDC issuer. Consumers should hard-code the builder workflow path so a compromised fork cannot produce acceptable signatures.
cosign verify \
--certificate-identity "https://github.com/acme/api/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/acme/api@$DIGEST | jq '.[0].optional.Bundle.Payload.logIndex'
Use --certificate-identity (exact match) not --certificate-identity-regexp when you can. A regex that accepts .* is equivalent to no verification at all and is the most common misconfiguration in the wild.
How do I enforce policy at admission?
Install Sigstore policy-controller and apply a ClusterImagePolicy that requires a keyless signature from your release workflow. The webhook rejects unsigned or mis-attested pods before they schedule.
helm repo add sigstore https://sigstore.github.io/helm-charts
helm install policy-controller sigstore/policy-controller \
--namespace cosign-system --create-namespace --version 0.9.0
kubectl label namespace production policy.sigstore.dev/include=true
kubectl apply -f - <<'EOF'
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata: { name: acme-release }
spec:
images:
- glob: ghcr.io/acme/**
authorities:
- keyless:
identities:
- issuer: https://token.actions.githubusercontent.com
subject: https://github.com/acme/api/.github/workflows/release.yml@refs/heads/main
mode: enforce
EOF
Start in mode: warn for the first week to surface false positives before flipping to enforce. A misconfigured policy that denies every pod in production is an outage, not a security win.
How Safeguard Helps
Safeguard treats Sigstore attestations as first-class supply chain evidence. When you push a signed image and attested SBOM, Safeguard pulls the Rekor entries, correlates them with your component inventory, and highlights which signed components are actually reachable in your deployed code — powered by Griffin AI's reachability analysis. The platform auto-ingests CycloneDX SBOMs carried as Sigstore attestations, so you do not need a separate SBOM upload pipeline. Policy gates can require a valid Cosign signature with a pinned identity before promotion, and failures open Jira tickets with the offending workflow and digest. Sigstore gives you the cryptographic facts; Safeguard turns them into enforceable deployment policy.