Container image signing closes a critical gap in the supply chain: without it, a registry compromise lets an attacker swap a trusted tag for a malicious digest and nobody notices. This tutorial shows you how to sign a container image with Cosign v2.2.x using keyless OIDC, push the signature alongside the image, verify it locally and in CI, and enforce a "signed-or-rejected" policy with Sigstore's policy-controller in Kubernetes. You will need Docker 24+, a container registry you can push to (GHCR, ECR, or Docker Hub), kubectl pointing at a dev cluster, and a GitHub or Google account for OIDC. Budget about 45 minutes — 10 for install, 10 for the first signed push, and the rest for admission policy and troubleshooting.
What tools do I need?
You need Cosign 2.2 or later, a container registry, and an OIDC identity provider. Install Cosign from the official release or via Homebrew, then confirm the binary is on PATH.
brew install cosign
cosign version
# GitVersion: 2.2.4
# GitCommit: 7a5d0a9...
# Platform: darwin/arm64
For Linux CI runners, pull the pinned release from GitHub and verify the checksum before using it. Avoid distro packages — they lag upstream by months and miss critical bugfixes in the verification path.
How do I sign my first image keylessly?
Run cosign sign against the image digest and complete the OIDC flow in your browser. Keyless mode issues a short-lived certificate from Fulcio tied to your identity, then records the signature in Rekor's transparency log.
IMAGE=ghcr.io/acme/api@sha256:a1b2c3d4e5f6...
cosign sign --yes $IMAGE
# Generating ephemeral keys...
# Retrieving signed certificate...
# tlog entry created with index: 89213476
# Pushing signature to: ghcr.io/acme/api
Always sign by digest, never by tag. Tags are mutable; a signature bound to a tag is worthless the moment someone force-pushes :latest. Capture the digest from docker buildx build --push output or from crane digest.
How do I verify a signed image?
Use cosign verify with the expected certificate identity and issuer. Verification fails closed if the tlog entry is missing, the cert is expired, or the identity does not match.
cosign verify \
--certificate-identity-regexp "https://github.com/acme/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/acme/api@sha256:a1b2c3d4e5f6... | jq '.[0].critical'
A successful run prints the signed payload including the image digest and a critical.type of cosign container image signature. Pipe through jq in CI logs so reviewers can spot mismatched digests at a glance.
How do I sign in GitHub Actions?
Use the official sigstore/cosign-installer action together with id-token: write permission so the workflow can mint an OIDC token. This is how keyless signing works without any long-lived secrets.
permissions:
contents: read
id-token: write
packages: write
steps:
- uses: sigstore/cosign-installer@v3.5.0
with:
cosign-release: 'v2.2.4'
- name: Sign image
run: cosign sign --yes ${IMAGE}@${DIGEST}
env:
IMAGE: ghcr.io/${{ github.repository }}
DIGEST: ${{ steps.build.outputs.digest }}
The id-token: write permission is the most common gotcha — without it, Cosign falls back to interactive OIDC and the workflow hangs forever. Pin the installer action to a full SHA in production.
How do I enforce signatures at admission?
Install the Sigstore policy-controller Helm chart and apply a ClusterImagePolicy that requires a keyless signature from your org's identity. The webhook rejects unsigned or misattested pods before they run.
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 clusterimagepolicy-acme.yaml
In clusterimagepolicy-acme.yaml, set spec.images[].glob to ghcr.io/acme/*, the authority to keyless with identities matching your CI workflow, and mode: enforce. Start in warn mode for a week and watch the admission webhook logs for false positives before flipping to enforce.
How do I troubleshoot verification failures?
Run cosign verify --output-file with COSIGN_EXPERIMENTAL=1 and inspect the Rekor entry directly. Most failures fall into three buckets: wrong identity regex, stale registry cache, or a signature signed against a tag that was then rewritten.
COSIGN_LOG_LEVEL=debug cosign verify \
--certificate-identity-regexp ".*" \
--certificate-oidc-issuer-regexp ".*" \
$IMAGE 2>&1 | tee cosign-debug.log
If tlog entry verification failed, the Rekor public key may be out of date — upgrade Cosign. If no matching signatures, your identity regex is too strict. Escape dots in the regex; . matches any character and often masks the real problem.
How Safeguard Helps
Safeguard turns signed-image metadata into actionable controls across your fleet. When you ingest a signed SBOM, Griffin AI correlates the Rekor entry with reachability data from Safeguard's static analysis so you know which signed components actually execute in production paths — not just sit on disk. The platform auto-generates CycloneDX SBOMs for signed images at push time and stores the attestation alongside vulnerability findings, so auditors see a single chain of custody. Policy gates can require a valid Cosign signature with a specific identity before a deploy is allowed to proceed, and violations open Jira tickets with the offending digest and suggested remediation. The result is signatures that gate deployment, not just decorate your registry.