Skip to content

Container Image Signing with Cosign: A Practical Guide

The SolarWinds breach was not a sophisticated zero-day. It was a build pipeline that let an attacker walk out with a signed, trusted update that every downstream customer installed without hesitation. Codecov had the same shape. 3CX had the same shape. The pattern is consistent: compromise the build, and every “we trust our registry” control evaporates simultaneously. If your deployment pipeline accepts any image that happens to be in your registry, you do not have a supply chain — you have a trust fall with no spotter.

This guide is for platform engineers, DevSecOps practitioners, and SREs who already run Kubernetes and a container registry and now need to prove — cryptographically, at admission time — that the images running in production came from their pipeline and nowhere else. By the end, you will have a working build → sign → verify → enforce pipeline using Cosign, both key-based and keyless, plus the operational reality that most tutorials skip entirely.

We are going to be blunt about what signing does and does not buy you, where the tooling bites, and why the single most common Cosign mistake (signing tags instead of digests) silently defeats the entire model.

Before starting, you need:

  • A container registry that supports arbitrary OCI artifacts (GHCR, ECR, GCR, Harbor, ACR all do; older Artifactory setups may need configuration).
  • cosign v2.x installed (we will install it from the signed release below).
  • Optionally: crane (from go-containerregistry) for digest lookups, syft for SBOM generation.
  • A Kubernetes cluster with Kyverno, Gatekeeper, or Sigstore Policy Controller installed if you plan to enforce.
  • For keyless: a CI platform that issues OIDC tokens (GitHub Actions, GitLab, GCP, Buildkite, CircleCI).
  • For KMS-backed keys: an AWS/GCP/Azure account or a HashiCorp Vault instance.

The Supply Chain Problem Nobody Wanted to Fix Until SolarWinds

Section titled “The Supply Chain Problem Nobody Wanted to Fix Until SolarWinds”

Let us be precise about the threat model, because the industry has been sloppy about it for a decade. Image signing gives you two properties: integrity (the bytes you pull are the bytes that were signed) and provenance (the identity that signed them is the identity you trust). It does not give you safety. A compromised build system that signs a backdoored image produces a perfectly valid signature. Cosign will verify it happily. Your admission controller will admit it. That is by design — signing is not a vulnerability scanner, and conflating the two is how teams end up with a false sense of security.

There are four distinct capabilities people lump under “signing,” and they solve different problems:

  1. Image integrity at rest — the signature over the manifest digest.
  2. Provenance attestation — a signed statement describing how the image was built (SLSA).
  3. SBOM attestation — a signed inventory of what is inside the image.
  4. Policy enforcement at admission — the control that actually prevents unsigned images from running.

You need all four for a complete story. Most teams implement one and call it done.

Why Cosign and Not Notary v1 / Docker Content Trust

Section titled “Why Cosign and Not Notary v1 / Docker Content Trust”

Docker Content Trust (DCT) was dead on arrival for reasons that had nothing to do with cryptography. It required a central Notary server that you had to operate. It only worked with Docker’s tooling. The UX was hostile enough that most teams disabled it (DOCKER_CONTENT_TRUST=0 was copy-pasted into more CI configs than anyone wants to admit). Notary v2 attempted to fix this but the ecosystem momentum went elsewhere.

Cosign won because of three design decisions: signatures are stored as OCI artifacts in the same registry as the image (no separate infrastructure), keyless signing via Sigstore removes the long-lived secret problem, and the tooling is a single static binary that works with any OCI-compliant registry. There is no server to run. That last point is why it actually got adopted.

A pipeline that builds an image, signs the digest with a KMS-backed key, attaches an SBOM attestation, and enforces signature presence at Kubernetes admission. Then we will do the same thing keylessly, and talk about when each is appropriate.

┌──────────────┐ push ┌──────────────┐ verify ┌────────────────────┐
│ CI Builder │ ──────────► │ Registry │ ◄────────── │ Admission Webhook │
│ (signs) │ │ (image+sig) │ │ (Kyverno/Gatekeeper)│
└──────┬───────┘ └──────────────┘ └─────────┬──────────┘
│ │
│ KMS/OIDC │ allow/deny
▼ ▼
┌──────────────┐ ┌────────────────────┐
│ KMS / Fulcio │ │ Kube API Server │
└──────────────┘ └────────────────────┘
FeatureDocker Content Trust (Notary v1)Notary v2Cosign
TransportCustom Notary protocolOCI artifactsOCI artifacts
Key modelRoot + delegation keysTUF rootKMS, file, keyless
Registry supportDocker Hub, limitedOCI-compliant (partial)Any OCI-compliant
KeylessNoNoYes (Fulcio/Rekor)
K8s integrationWeakEmergingStrong (Kyverno, Gatekeeper, SPC)
Maintenance statusEffectively abandonedStalledActively maintained

Q: Isn’t signing just TLS for images? No. TLS protects the transport between you and the registry. Signing protects the artifact itself, end-to-end, regardless of transport. A man-in-the-middle who can terminate TLS still cannot forge a Cosign signature without the private key.

Q: Do I need Sigstore to use Cosign? No. Cosign works entirely standalone with local or KMS-managed keys. Sigstore (Fulcio + Rekor) is only required for the keyless flow. You can run key-based Cosign in a fully air-gapped environment.


Installing Cosign and Understanding the Key Model

Section titled “Installing Cosign and Understanding the Key Model”

Distribution package managers lag badly. apt install cosign on Debian stable is either absent or multiple minor versions behind, and the version mismatch will bite you when a flag you read about in the docs does not exist. Pull the signed release binary from GitHub and verify it with its own signature — yes, the irony is noted, but the release artifacts are signed keylessly by the Sigstore release pipeline, which is exactly the model we are advocating.

Terminal window
# Install and verify cosign from the signed release
curl -sLO https://github.com/sigstore/cosign/releases/download/v2.2.4/cosign-linux-amd64
curl -sLO https://github.com/sigstore/cosign/releases/download/v2.2.4/cosign-linux-amd64.sig
curl -sLO https://github.com/sigstore/cosign/releases/download/v2.2.4/cosign-linux-amd64.pem
cosign verify-blob \
--certificate cosign-linux-amd64.pem \
--signature cosign-linux-amd64.sig \
--certificate-identity keyless@projectsigstore.iam.gserviceaccount.com \
--certificate-oidc-issuer https://accounts.google.com \
cosign-linux-amd64
chmod +x cosign-linux-amd64 && sudo mv cosign-linux-amd64 /usr/local/bin/cosign

That verification chain checks the signature against a certificate bound to the Sigstore release identity. If it succeeds, you know the binary came from the Sigstore project’s release pipeline and not from a compromised CDN.

Key-Based vs. Keyless — Pick Based on Your Threat Model

Section titled “Key-Based vs. Keyless — Pick Based on Your Threat Model”

Key-based means you generate a keypair, you own the private key, you own the rotation, and you own the breach when it leaks. This is correct for air-gapped environments and regulated shops where depending on public Sigstore infrastructure is a non-starter.

Keyless means Cosign uses an OIDC token from your CI (GitHub Actions, GCP workload identity, GitLab id_tokens) to obtain a short-lived certificate from Fulcio, signs, and logs the signature to Rekor. There is no long-lived secret to leak. The trade-off is that you now depend on Sigstore’s availability and trust its transparency log.

The bad practice that refuses to die: storing the private key as a base64-encoded CI secret in an environment variable. This is theater. Use a KMS — AWS KMS, GCP KMS, Azure Key Vault, or HashiCorp Vault. Cosign supports all of them via --key with a scheme prefix.

Terminal window
# Generate a keypair where the private key never leaves AWS KMS
cosign generate-key-pair --kms awskms:///alias/cosign-signing
# The public key is written locally as cosign.pub — that is what verifiers need

The private key material is generated inside KMS and never exported. Signing operations are API calls to KMS. If your CI runner is compromised, the attacker can use the key while they have access, but they cannot exfiltrate it and sign images six months later from a different network.

For GitHub Actions keyless, the only thing you need is an OIDC token permission:

permissions:
id-token: write # required for keyless OIDC
contents: read
packages: write
AspectKey-basedKeyless
Key custodyYou (KMS strongly recommended)Fulcio issues short-lived certs
RotationManual, plannedAutomatic (certs expire in ~10 min)
Air-gapWorksRequires private Sigstore deployment
Audit trailYour KMS logsRekor transparency log
CI complexityKMS auth setupOIDC permission only
Blast radius on compromiseFull until key revokedLimited to cert lifetime + token scope

Q: Can I rotate keys without breaking existing signatures? Yes, but not by re-signing. Existing signatures remain valid under the old public key. Rotation means publishing a new public key, signing new images with it, and keeping the old public key in your verification policy until every image signed with it is out of rotation.

Q: What happens if Sigstore’s Rekor goes down? Verification of keyless signatures that require a Rekor lookup will fail unless you have cached the entry or use --offline with a local bundle. This is a real operational concern for keyless adoption — see the Rekor dependency section below.


Signing Your First Image — The Happy Path and Its Traps

Section titled “Signing Your First Image — The Happy Path and Its Traps”

The single most common Cosign mistake is signing a tag instead of a digest. Tags are mutable. Anyone with push access can retag latest to a different image, and your signature over the tag reference is now attached to something you never signed. Always resolve to a digest first and sign that.

Terminal window
# Resolve the tag to an immutable digest, then sign the digest
DIGEST=$(crane digest ghcr.io/org/app:1.2.3)
cosign sign --key awskms:///alias/cosign-signing ghcr.io/org/app@${DIGEST}

Cosign pushes a new OCI artifact to the same registry, tagged as sha256-[digest].sig. That artifact contains the signature, and for keyless, the certificate chain and Rekor bundle.

Where Signatures Live — And Why You Should Care

Section titled “Where Signatures Live — And Why You Should Care”

Because signatures are OCI artifacts in the same registry, your registry must support arbitrary artifact types. ECR, GHCR, GCR, ACR, and Harbor all do. Some older Artifactory configurations require enabling this explicitly.

The war story: a team signed everything faithfully for six months, then discovered their registry garbage collector was pruning .sig tags because they were not referenced by any manifest list. Signatures disappeared silently. Admission started rejecting deploys at 2 AM. They spent four hours thinking Cosign was broken before someone checked the GC logs.

Fix: configure your registry’s GC to retain OCI artifacts referenced by your signing convention, or use a registry that handles this correctly. Test it before you commit.

A signature alone proves the image came from you. An attestation proves what is inside it or how it was built. This is what lets you enforce “no image deploys without an SBOM” at admission.

Terminal window
# Generate an SBOM and attach it as a signed attestation
syft ghcr.io/org/app@${DIGEST} -o spdx-json > sbom.spdx.json
cosign attest --key awskms:///alias/cosign-signing \
--predicate sbom.spdx.json --type spdxjson \
ghcr.io/org/app@${DIGEST}
# Verify both signature and attestation
cosign verify --key cosign.pub ghcr.io/org/app@${DIGEST}
cosign verify-attestation --key cosign.pub --type spdxjson ghcr.io/org/app@${DIGEST}

For SLSA provenance, use --type slsaprovenance and pass a predicate generated by your build system (GitHub Actions has a SLSA generator action; Tekton and others have equivalents).

ArtifactRegistry tag patternVerifier commandGC risk
Signaturesha256-[digest].sigcosign verifyHigh if GC ignores OCI artifacts
SBOM attestationsha256-[digest].attcosign verify-attestation --type spdxjsonSame
SLSA provenancesha256-[digest].attcosign verify-attestation --type slsaprovenanceSame
Keyless bundleinside .sigcosign verify --certificate-identity ...Same

Q: Why did my signature disappear after a registry cleanup? Because .sig artifacts are not referenced by manifest lists, some registry garbage collectors treat them as orphaned and prune them. Configure retention rules or use a registry that understands Cosign’s convention.

Q: Can I sign an image I don’t own? Technically you can push a signature to any repository you have write access to, but verification against your identity will only succeed if the verifier trusts your key or identity. Signing someone else’s image is not a useful attack — it just produces a signature nobody trusts.


Keyless Signing with Sigstore — When It’s Right and When It Bites You

Section titled “Keyless Signing with Sigstore — When It’s Right and When It Bites You”

Keyless signing is the feature that made Cosign mainstream. It solves the “where do I keep the private key” problem by making the key ephemeral and binding the signature to an OIDC identity instead.

Your CI job requests an OIDC token from its platform (GitHub, GCP, etc.). Cosign presents that token to Fulcio, which validates it and issues a short-lived X.509 certificate (typically valid for around 10 minutes) binding the token’s identity to a public key. Cosign signs the image digest with the corresponding private key, and logs the signature + certificate to Rekor, the transparency log. The private key is discarded. Verification checks the signature against the certificate, and the certificate against Fulcio’s root, and the Rekor entry for tamper-evidence.

The identity embedded in the certificate is the trust anchor. That is what your verification policy checks.

Verifying Keyless Signatures — The Regexp Is Your Policy

Section titled “Verifying Keyless Signatures — The Regexp Is Your Policy”
Terminal window
# Strict keyless verification — the identity regexp IS your policy
cosign verify \
--certificate-identity-regexp '^https://github.com/myorg/myrepo/.github/workflows/release.yml@refs/heads/main$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/org/app@${DIGEST}

A permissive regexp like .*@github.com is functionally identical to no verification at all. Anyone with a GitHub account can fork your workflow and produce a signature that matches that pattern. This is the most common keyless misconfiguration in the wild, and it silently defeats the entire model. Anchor your regexp to the exact workflow path and ref.

# GitHub Actions keyless signing — the canonical flow
- uses: sigstore/cosign-installer@v3
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Sign keylessly
run: cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}

Rekor is a public transparency log. It is also a dependency. If Rekor is unreachable at verification time and you have not cached the entry, verification fails. This is a real availability concern for production admission controllers.

Mitigations:

  • --rekor-url — point at a private Rekor instance (Sigstore Scaffold supports this).
  • --offline — verify against a cached bundle without a Rekor lookup.
  • --insecure-ignore-tlog — skip the Rekor check entirely. This is a footgun: it removes the tamper-evidence property that makes keyless valuable. Do not use it in production.

For regulated environments, run a private Sigstore deployment. It is more work than most teams expect, but it removes the public infrastructure dependency entirely.

FlagEffectWhen to useFootgun level
--certificate-identity-regexpAnchors verification to a specific OIDC identityAlwaysN/A — required
--certificate-oidc-issuerRestricts to a specific issuerAlwaysN/A — required
--rekor-urlUse a private Rekor instanceAir-gap / regulatedLow
--offlineVerify from cached bundle, no Rekor callAdmission with cacheLow
--insecure-ignore-tlogSkip Rekor verification entirelyNever in prodHigh

Q: Can I use keyless in an air-gapped environment? Not against public Sigstore. You need a private Fulcio and Rekor deployment (Sigstore Scaffold is the reference implementation). This is a significant operational investment — for most air-gapped shops, KMS-backed keys are the pragmatic choice.

Q: What’s the difference between Fulcio and Rekor? Fulcio is the certificate authority that issues short-lived signing certs bound to OIDC identities. Rekor is the append-only transparency log that records signatures so they can be audited and so that a compromised Fulcio cannot silently forge signatures retroactively.


Enforcing Signatures at Admission — Where the Rubber Meets the Cluster

Section titled “Enforcing Signatures at Admission — Where the Rubber Meets the Cluster”

Signing without enforcement is a compliance checkbox. The value comes from a policy engine that refuses to admit unsigned images. You have three realistic options.

Sigstore Policy Controller is the lowest-friction choice if you are all-in on Cosign. It is purpose-built for this and the config is minimal. Kyverno is more flexible — you will write more YAML, but you can combine signature verification with other policies in one engine. Gatekeeper is the right choice if you already run OPA for other reasons; do not adopt OPA just for image verification.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-cosign-signatures
spec:
validationFailureAction: Audit # change to Enforce after baseline
background: true
rules:
- name: verify-signature
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "ghcr.io/org/*"
mutateDigest: true # rewrite tags to digests — underrated
verifyDigest: true
required: true
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
attestations:
- predicateType: https://spdx.dev/Document
conditions:
- all:
- key: "{{ name }}"
operator: NotEquals
value: ""

Two things worth calling out. First, mutateDigest: true rewrites image tags to digests at admission — this is underrated and closes the tag-mutation gap that signing alone does not. Second, the attestation block requires the SPDX SBOM to be present, so an image with a signature but no SBOM is rejected.

Never flip to enforce on day one. Start with validationFailureAction: Audit, watch the policy reports, fix the stragglers, then enforce. The war story is always the same: a team enforced on prod first, and every sidecar, every init container, every Helm chart that pulled bitnami/kubectl:latest broke simultaneously. Their incident channel was a warzone for six hours.

Terminal window
# Watch the audit reports before you enforce
kubectl get policyreport -A | grep verify-cosign
kubectl get clusterpolicyreport -o yaml | grep -A5 "verify-cosign-signatures"
EngineConfig languageKeylessAttestationMutationOps cost
Sigstore Policy ControllerCRDsYesYesLimitedLow
KyvernoYAMLYesYesYesMedium
Gatekeeper (OPA)RegoYesYesYesHigh if new to Rego

Q: What about images from Docker Hub that I don’t control? You cannot verify signatures on images you did not sign. Options: mirror them into your registry and sign the mirror (you are now attesting to the mirror’s contents), or exempt specific trusted upstreams from the policy and accept the risk explicitly.

Q: How do I verify signatures in a multi-tenant cluster? Use namespace-scoped policies with different trust roots per tenant, or run separate policy engines per tenant boundary. A single cluster-wide policy with one trust root is a design smell in multi-tenant environments.


CI/CD Integration — Signing Without Slowing Down the Pipeline

Section titled “CI/CD Integration — Signing Without Slowing Down the Pipeline”

Signing must happen after the image is pushed, because you cannot sign a digest that does not exist in the registry yet. This is not optional — there is no “sign then push” flow that works with Cosign’s model.

# GitHub Actions: build, push, sign, attest — the canonical flow
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign image
run: cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
- name: Generate and attest SBOM
run: |
syft ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} -o spdx-json > sbom.json
cosign attest --yes --predicate sbom.json --type spdxjson \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}

Signing is fast — typically under a second for the cryptographic operation plus the registry write. The SBOM generation step is the slow part in most pipelines.

GitLab CI, Jenkins, and the “My CI Has No OIDC” Problem

Section titled “GitLab CI, Jenkins, and the “My CI Has No OIDC” Problem”

GitLab supports OIDC via id_tokens: on recent versions. Self-hosted GitLab on older releases may not. The fallback is a KMS-backed key, which is honestly fine.

# GitLab CI keyless signing
sign:
image: gcr.io/projectsigstore/cosign:latest
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
script:
- cosign sign --yes $CI_REGISTRY_IMAGE@$DIGEST

Jenkins: unless you are on a recent version with an OIDC plugin, use a KMS key. Do not store the private key in Jenkins credentials. That is a plaintext secret with extra steps. Use Vault or cloud KMS.

Signature pushes are registry writes. On ECR, that is per-request cost. At scale — hundreds of builds per day — this adds up, though it is usually a rounding error compared to your image storage bill. Verification at admission is the more expensive operation: each admission triggers a registry pull of the signature and potentially a Rekor lookup. Cache aggressively and use --offline where you can.

PlatformOIDC availableKeylessKMS integrationGotchas
GitHub ActionsYesYesYesOIDC token audience must be sigstore
GitLab CIYes (recent versions)YesYesid_tokens: requires GitLab 15.7+
JenkinsPlugin-dependentLimitedYes via Vault/KMSNo native OIDC in older versions
CircleCIYesYesYesOIDC context required
BuildkiteYesYesYesAgent must expose OIDC token

Q: How do I sign multi-arch images? Sign the manifest list digest, not the per-architecture manifest digests. docker buildx imagetools inspect or crane digest on the multi-arch tag gives you the right digest. Signing individual platform manifests leaves the manifest list itself unsigned.

Q: Do I need to sign every layer or just the manifest? Just the manifest. The manifest contains the digests of the config and layers, so signing the manifest transitively covers everything below it. Signing layers individually is not a thing Cosign does and is not necessary.


Operational Reality — Rotation, Revocation, and the Stuff Docs Skip

Section titled “Operational Reality — Rotation, Revocation, and the Stuff Docs Skip”

You cannot un-sign an image. Rotation means: publish the new key, sign new images with it, and keep the old public key in your verification policy until every image signed with it is out of rotation. During the transition window, your policy accepts both keys.

# Kyverno policy accepting two keys during a rotation window
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...new key...
-----END PUBLIC KEY-----
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...old key...
-----END PUBLIC KEY-----

Once you have confirmed via policy reports that no running image was signed with the old key, remove it. If you skip that confirmation step, you will discover the hard way that some long-lived CronJob was signed with the old key and now refuses to schedule.

Revocation — Cosign Doesn’t Really Do It

Section titled “Revocation — Cosign Doesn’t Really Do It”

There is no CRL for image signatures. If a signing key is compromised, you revoke trust at the policy layer — remove the key from the admission controller — not at the signature layer. Every image signed with that key immediately becomes untrusted, including legitimate ones. This is a design limitation, not a bug: signatures are immutable facts, and trust is a policy decision. Plan for it. Have a documented procedure for “key X is compromised, here is how we remove it from policy and re-sign affected images.”

Alert on unsigned images appearing in prod namespaces, verification failures, Rekor unreachability, and signature artifact deletion events in the registry. The last one is your early warning for the GC problem.

# Prometheus alert for admission failures
- alert: UnsignedImageDetected
expr: increase(kyverno_policy_results_total{policy="verify-cosign-signatures",result="fail"}[5m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: "Unsigned image attempted in cluster"

Q: Can I revoke a single signature? No. You can remove the signing key from your trust policy, which invalidates all signatures made with that key. There is no per-signature revocation mechanism.

Q: What happens to signatures when I delete an image? The signature artifact typically remains in the registry unless your GC handles it. This is usually harmless but can cause confusion — a signature with no corresponding image. Clean up both explicitly if your registry does not do it for you.


  1. Signing tags instead of digests. Tags are mutable. A signature over a tag is a signature over nothing. Always resolve to a digest with crane digest or the build action’s digest output, and sign that.

  2. Permissive keyless identity regexps. .*@github.com matches every GitHub user on the planet. Anchor your regexp to the exact workflow path and ref: ^https://github.com/org/repo/.github/workflows/release.yml@refs/heads/main$.

  3. Storing private keys in CI secrets. A base64 key in an environment variable is a plaintext secret that leaks the moment your CI runner is compromised. Use KMS. There is no good reason not to.

  4. Enforcing at admission before auditing. Flipping validationFailureAction to Enforce on day one breaks every unsigned image in your cluster simultaneously, including init containers and sidecars you forgot existed. Audit for at least a week, fix the stragglers, then enforce.

  5. Ignoring registry GC behavior. Some registries prune .sig and .att artifacts as orphans. Test this before you commit to a signing strategy, or you will discover it at 2 AM when admission starts rejecting deploys.

  6. Using --insecure-ignore-tlog in production. This disables the transparency log check, which is the property that makes keyless signatures tamper-evident. If you need it to make verification work, you have a different problem to solve.


Q: Is Cosign production-ready? Yes. It is used at scale by Google, GitHub, Chainguard, and most of the Kubernetes ecosystem. The tooling is mature. The operational discipline around key management and policy enforcement is where teams get into trouble, not the tool.

Q: What’s the minimum viable setup for a small team? Keyless signing in GitHub Actions with a strict identity regexp, Sigstore Policy Controller in audit mode for a week, then enforce. Skip SBOM attestation initially if you are resource-constrained — add it once signatures are working. You can grow into the full model.

Q: Do I need to sign images in development environments? No, but you should sign in staging. The point of signing is to catch pipeline problems before production. Signing only in prod means you discover misconfigurations during a deploy.

Q: How does Cosign interact with image vulnerability scanning? They are orthogonal. Cosign proves provenance and integrity; scanners like Trivy, Grype, or Snyk find vulnerabilities. You can — and should — enforce both at admission: “signed AND no critical CVEs” is a much stronger gate than either alone.

Q: What’s the difference between a signature and an attestation? A signature says “this exact image digest was signed by this identity.” An attestation says “this exact image digest was signed by this identity, and here is a signed statement about it” — the statement being an SBOM, SLSA provenance, or a custom predicate. Attestations are signatures with content.


Actionable Takeaways and the Checklist You’ll Actually Use

Section titled “Actionable Takeaways and the Checklist You’ll Actually Use”
  1. Sign digests, never tags.
  2. Store keys in KMS, never in CI environment variables.
  3. Use strict identity regexps for keyless — no wildcards, no .*@github.com.
  4. Attach SBOMs, not just signatures — enforce their presence at admission.
  5. Audit before you enforce at admission. Always. No exceptions.
  6. Alert on verification failures, not just on policy violations.
  7. Plan key rotation before you need it, and document the procedure.
  8. Understand that revocation is a policy action, not a cryptographic one.
  9. Test your registry’s OCI artifact support and GC behavior before you commit.
  10. Document your trust root — future you will not remember which key signed what.

The Sigstore documentation is the canonical reference. The SLSA specification explains the provenance model. Kyverno’s verifyImages docs cover the policy engine in detail. And read your registry’s documentation on OCI artifact support and garbage collection — that is where the surprises live.

Now go sign something. And when you do, sign the digest.