· Updated

Skopeo for Image Verification

Using Docker Alternative Skopeo and Cosign for Image Verification

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:

Skopeo in CI/CD

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 cosign


Workflow: 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-----

Stay tuned for part 3: Skopeo in Air-Gapped environments

---
title: "Skopeo in CI/CD"
description: "Using Docker Alternative Skopeo"
pubDate: 2026-04-26
updatedDate: 2026-04-26 # optional
tags: ["docker", "kubernetes", "podman", "tech", "skopeo", "cicd", "pipelines", "devops", "cosign"]
draft: false # drafts are visible in dev, dropped from builds
---