Cosign for Container Image Signing and Verification

Cosign is a tool from the Sigstore project that enables container image signing and verification to protect the software supply chain, supporting both key-based and keyless signing with Fulcio and Rekor. This guide covers installing Cosign, signing images with key pairs, using keyless signing in CI/CD, and enforcing signature verification policies.

Prerequisites

  • Linux server (Ubuntu 22.04/Debian 12 or CentOS/Rocky 9)
  • Docker or Podman for building images
  • Access to a container registry (Docker Hub, GHCR, ECR, etc.)
  • A CI/CD provider (GitHub Actions, GitLab CI) for keyless signing

Install Cosign

# Download the latest Cosign binary
COSIGN_VERSION=$(curl -s https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d'"' -f4)
curl -L "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" \
  -o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign

# Verify the Cosign binary itself (bootstrapping trust)
curl -L "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64.sig" \
  -o /tmp/cosign.sig
curl -L "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign_checksums.txt" \
  -o /tmp/cosign_checksums.txt

# Or install via package manager
# Ubuntu/Debian
sudo apt install -y cosign

# Check version
cosign version

Key-Based Image Signing

Generate a key pair for signing images:

# Generate a cosign key pair
# Will prompt for a password to protect the private key
cosign generate-key-pair

# Files created:
# cosign.key  - private key (keep secret!)
# cosign.pub  - public key (distribute widely for verification)

# Store the private key password in an environment variable or secret manager
export COSIGN_PASSWORD="your-strong-password"

# Generate a key pair non-interactively
COSIGN_PASSWORD=your-password cosign generate-key-pair

# Store keys in Kubernetes secrets
kubectl create secret generic cosign-keys \
  --from-file=cosign.pub=./cosign.pub \
  --from-file=cosign.key=./cosign.key \
  -n kube-system

Sign a container image:

# Build and push the image first
docker build -t registry.example.com/myapp:v1.0.0 .
docker push registry.example.com/myapp:v1.0.0

# Sign using the image tag (NOT recommended - tags are mutable)
COSIGN_PASSWORD=your-password cosign sign \
  --key cosign.key \
  registry.example.com/myapp:v1.0.0

# BETTER: Sign using the image digest (immutable reference)
# Get the digest after pushing
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' registry.example.com/myapp:v1.0.0)
echo "Image digest: ${DIGEST}"

COSIGN_PASSWORD=your-password cosign sign \
  --key cosign.key \
  ${DIGEST}

# Sign with custom annotations (metadata)
COSIGN_PASSWORD=your-password cosign sign \
  --key cosign.key \
  --annotations "repo=https://github.com/org/repo" \
  --annotations "git-commit=$(git rev-parse HEAD)" \
  --annotations "build-date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  registry.example.com/myapp:v1.0.0

Verify Signed Images

# Verify with the public key
cosign verify \
  --key cosign.pub \
  registry.example.com/myapp:v1.0.0

# Expected output includes the signature and claims:
# {"critical":{"identity":{"docker-reference":"registry.example.com/myapp"},...}}

# Verify specific annotations
cosign verify \
  --key cosign.pub \
  --annotations "repo=https://github.com/org/repo" \
  registry.example.com/myapp:v1.0.0

# Verify by digest
cosign verify \
  --key cosign.pub \
  registry.example.com/myapp@sha256:abc123...

# Show signature details
cosign verify --key cosign.pub \
  registry.example.com/myapp:v1.0.0 | jq '.'

Keyless Signing with Sigstore

Keyless signing uses OIDC tokens (from GitHub Actions, GitLab CI, etc.) and the Sigstore public good infrastructure (Fulcio CA + Rekor transparency log), eliminating the need to manage private keys:

# Keyless sign (requires OIDC token from a supported provider)
# This works in GitHub Actions, GitLab CI, Google Cloud Build, etc.
COSIGN_EXPERIMENTAL=1 cosign sign \
  --yes \
  registry.example.com/myapp:v1.0.0

# Keyless verify - specify the expected OIDC issuer and subject
COSIGN_EXPERIMENTAL=1 cosign verify \
  --certificate-identity-regexp=".*@github.com" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  registry.example.com/myapp:v1.0.0

# Verify with exact identity (recommended for production)
COSIGN_EXPERIMENTAL=1 cosign verify \
  --certificate-identity="https://github.com/org/repo/.github/workflows/build.yml@refs/heads/main" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  registry.example.com/myapp:v1.0.0

CI/CD Integration

GitHub Actions with keyless signing:

# .github/workflows/build-and-sign.yml
name: Build, Push, and Sign

on:
  push:
    branches: [main]
    tags: ["v*"]

permissions:
  contents: read
  packages: write
  id-token: write   # Required for keyless signing (OIDC token)

jobs:
  build-and-sign:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install Cosign
        uses: sigstore/cosign-installer@v3

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push image
        id: build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

      - name: Sign the image (keyless)
        run: |
          cosign sign \
            --yes \
            "ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}"
        env:
          COSIGN_EXPERIMENTAL: "1"

      - name: Verify the signature
        run: |
          cosign verify \
            --certificate-identity-regexp="https://github.com/${{ github.repository }}" \
            --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
            "ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}"
        env:
          COSIGN_EXPERIMENTAL: "1"

GitLab CI with key-based signing:

# .gitlab-ci.yml
stages:
  - build
  - sign
  - deploy

variables:
  IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

build:
  stage: build
  script:
    - docker build -t $IMAGE .
    - docker push $IMAGE

sign-image:
  stage: sign
  image:
    name: gcr.io/projectsigstore/cosign:latest
    entrypoint: [""]
  script:
    - cosign sign --key $COSIGN_PRIVATE_KEY $IMAGE
  variables:
    COSIGN_PASSWORD: $COSIGN_KEY_PASSWORD

verify-image:
  stage: deploy
  script:
    - cosign verify --key $COSIGN_PUBLIC_KEY $IMAGE
    - echo "Image verified, proceeding with deployment"

Sign and Verify SBOMs and Attestations

Attach and sign attestations (provenance, SBOM) to images:

# Generate SBOM with Trivy
trivy image --format cyclonedx --output sbom.json myapp:v1.0.0

# Attach and sign the SBOM as an attestation
COSIGN_PASSWORD=your-password cosign attest \
  --key cosign.key \
  --predicate sbom.json \
  --type cyclonedx \
  registry.example.com/myapp:v1.0.0

# Verify and retrieve the attestation
cosign verify-attestation \
  --key cosign.pub \
  --type cyclonedx \
  registry.example.com/myapp:v1.0.0

# Sign build provenance (SLSA)
cosign attest \
  --key cosign.key \
  --predicate provenance.json \
  --type slsaprovenance \
  registry.example.com/myapp:v1.0.0

# List all signatures and attestations on an image
cosign tree registry.example.com/myapp:v1.0.0

Enforce Verification in Kubernetes

Use Kyverno or Connaisseur to enforce that all pods use signed images:

With Kyverno:

# kyverno-verify-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: verify-cosign-signature
      match:
        any:
        - resources:
            kinds: ["Pod"]
            namespaces: ["production"]
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - count: 1
              entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      YOUR_COSIGN_PUBLIC_KEY_HERE
                      -----END PUBLIC KEY-----
kubectl apply -f kyverno-verify-policy.yaml

# Test enforcement - unsigned image should be rejected
kubectl run test --image=registry.example.com/unsigned-image:latest
# Expected: policy violation

# Signed image should succeed
kubectl run test --image=registry.example.com/myapp:v1.0.0

With Connaisseur (dedicated admission controller for image verification):

helm repo add connaisseur https://sse-secure-systems.github.io/connaisseur/charts
helm install connaisseur connaisseur/connaisseur \
  --namespace connaisseur \
  --create-namespace \
  --set validators[0].name=cosign \
  --set validators[0].type=cosign \
  --set validators[0].publicKey="$(cat cosign.pub)"

Troubleshooting

Error: "no signatures found":

# Verify the image was signed and pushed
cosign triangulate registry.example.com/myapp:v1.0.0
# This shows the signature OCI tag (e.g., sha256-abc.sig)

# Check if the signature tag exists in the registry
docker manifest inspect registry.example.com/myapp:sha256-abc.sig

Keyless signing fails - OIDC token not found:

# Ensure the CI/CD environment provides OIDC tokens
# For GitHub Actions, add: permissions: id-token: write

# Test OIDC token availability
curl -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
  "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore"

Verification fails with wrong identity:

# Check the exact OIDC identity in the signature
COSIGN_EXPERIMENTAL=1 cosign verify \
  --certificate-identity-regexp=".*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  registry.example.com/myapp:v1.0.0 | jq '.[0].optional'
# Use the exact values shown in the output for --certificate-identity

Private key passphrase not accepted:

# Set the passphrase in the environment
export COSIGN_PASSWORD="your-passphrase"
cosign sign --key cosign.key registry.example.com/myapp:v1.0.0

Conclusion

Cosign provides a practical approach to container image signing that fits into existing CI/CD workflows with minimal friction. Keyless signing with GitHub Actions and Sigstore's public infrastructure eliminates private key management entirely, recording signatures in the Rekor transparency log for public auditability. For private environments or stricter supply chain requirements, key-based signing with Kyverno or Connaisseur policy enforcement ensures that only images signed by trusted keys can run in Kubernetes clusters.