Skopeo + Cosign for image verification in your registry workflow
Published for Platform / SRE teamsย |ย ~800 words | Part 2
If you havenโt read part 1 of this 3 part series:
Why image signing matters for SRE teams
A container registry is only as trustworthy as the images it holds. Without signing, there is no cryptographic guarantee that the image you pull matches the one your build pipeline produced. Skopeo handles the transport; Cosign handles the attestation.
Tools
- skopeo โ copy and inspect images across registries
- cosign (Sigstore) โ sign, verify, and attach attestations to OCI artifacts
- rekor (Sigstore) โ (optional) transparency log for signatures (sigstore.dev)
Installing Cosign
# Linux (amd64)
curl -sSfL https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 -o /usr/local/bin/cosign && chmod +x /usr/local/bin/cosign# Verify the cosign binary itself (bootstrap trust)
cosign verify-blob --certificate-identity=... cosign-linux-amd64# macOS
brew install cosignWorkflow: sign after build, verify before deploy
The signing and verification steps are separate. (co)Sign in your build pipeline; verify in your admission controller or deploy pipeline.
Step 1: Generate a key pair (or use keyless)
# Key-based (store private key in a secret manager) cosign generate-key-pair # Produces: cosign.key (private), cosign.pub (public) # Keyless (uses OIDC โ works well with GitHub Actions, GKE Workload Identity) # No key management needed; identity is bound to the OIDC token
Step 2: Copy the image with Skopeo, then sign
IMAGE=registry.internal.example.com/app:$GIT_SHA # Copy the image into your registry skopeo copy \ --authfile /tmp/auth.json docker://build-cache.internal/app:$GIT_SHA \ docker://$IMAGE # Pin to the digest (sign the digest, not the mutable tag) DIGEST=$(skopeo inspect --format '{{.Digest}}' docker://$IMAGE) # Sign cosign sign --key cosign.key registry.internal.example.com/app@$DIGEST
Tip: Always sign the digest, not the tag. Tags are mutable. Signing a digest gives you a tamper-evident guarantee tied to specific image content.
Step 3: Verify before deploying
IMAGE=registry.internal.example.com/app:$GIT_SHA DIGEST=$(skopeo inspect --format '{{.Digest}}' docker://$IMAGE) cosign verify \ --key cosign.pub \ registry.internal.example.com/app@$DIGEST # Non-zero exit code = verification failed = block the deploy
Keyless signing in GitHub Actions
Keyless mode uses GitHub's OIDC token to bind the signature to the workflow identity. No secrets to manage.
- name: Sign Image env: COSIGN_EXPERIMENTAL: '1' ย # enables keyless run: | DIGEST=$(skopeo inspect --raw \ docker://ghcr.io/${{ github.repository }}:${{ github.sha }} \ | jq -r '.config.digest') cosign sign \ ghcr.io/${{ github.repository }}@$DIGEST - name: Verify image env: COSIGN_EXPERIMENTAL: '1' run: | cosign verify \ --certificate-identity-regexp 'https://github.com/${{ github.repository }}' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ ghcr.io/${{ github.repository }}@$DIGEST
Attaching SBOMs as OCI attestations
# Generate SBOM with syft syft registry.internal.example.com/app:$GIT_SHA -o spdx-json > sbom.spdx.json # Attach as attestation cosign attest --key cosign.key \ --predicate sbom.spdx.json \ --type spdxjson \ registry.internal.example.com/app@$DIGEST # Copy image + attestations to another registry skopeo copy --all \ docker://registry.internal.example.com/app:$GIT_SHA \ docker://prod-registry.example.com/app:$GIT_SHA # --all includes the cosign signature and attestation OCI tags
Enforcing verification in Kubernetes
Use Kyverno or Sigstore Policy Controller to reject pods whose images are unsigned or have an invalid signature:
# Kyverno ClusterPolicy snippet rules: - name: verify-image-signature match: resources: kinds: [Pod] verifyImages: - imageReferences: - 'registry.internal.example.com/*' attestors: - entries: - keys: publicKeys: |- -----BEGIN PUBLIC KEY----- <your cosign.pub content> -----END PUBLIC KEY-----
