Container Security

Container Image Signing with Cosign: A Practical Deep Dive

Cosign makes signing and verifying container images straightforward. Here's everything you need to know to implement it in your pipeline.

Alex
DevSecOps Engineer
6 min read

Container images are the most common deployment artifact in modern software. They're also one of the easiest to tamper with. Between the registry, the build system, and the runtime environment, a container image passes through multiple points where it could be modified, replaced, or substituted. Without cryptographic signing, you're trusting every intermediary in that chain.

Cosign, part of the Sigstore project, makes container image signing practical. Not theoretical. Not "we'll get to it." Actually practical, with a developer experience that doesn't make you want to abandon the whole effort.

Why Sign Container Images?

Before diving into the how, let's be clear about the why:

Tamper detection. A signed image can be verified at deployment time. If the image was modified after signing — whether by a compromised registry, a man-in-the-middle attack, or an insider threat — the signature won't verify.

Provenance. Signatures can attest to who built the image and from what source. This is the foundation for answering "where did this image come from?" — a question that's surprisingly hard to answer without signing.

Policy enforcement. Kubernetes admission controllers can verify signatures before allowing images to run in a cluster. This creates a hard gate: only signed images from trusted sources can be deployed.

Compliance. Frameworks like SLSA, NIST SSDF, and various industry regulations increasingly require artifact signing. Container image signing is the most direct way to meet these requirements for containerized workloads.

Cosign Modes: Key-Based vs. Keyless

Cosign supports two signing modes, each with different tradeoffs:

Key-Based Signing

Traditional approach where you generate a key pair and sign with the private key:

# Generate a key pair
cosign generate-key-pair

# Sign an image
cosign sign --key cosign.key registry.example.com/myapp:v1.0

# Verify a signature
cosign verify --key cosign.pub registry.example.com/myapp:v1.0

Pros: Simple to understand, works offline, no dependency on external services. Cons: You need to manage the private key (storage, rotation, access control). Key compromise requires re-signing all images.

Keyless Signing (Recommended)

Uses Sigstore's Fulcio CA to issue short-lived certificates based on OIDC identity:

# Sign an image (opens browser for OIDC auth)
cosign sign registry.example.com/myapp:v1.0

# Verify with identity constraints
cosign verify \
  --certificate-identity user@example.com \
  --certificate-oidc-issuer https://accounts.google.com \
  registry.example.com/myapp:v1.0

Pros: No key management, identity-based trust, signatures recorded in transparency log. Cons: Requires network access to Sigstore services during signing, identity must be configured for verification.

For CI/CD pipelines, keyless signing with workload identity (GitHub Actions OIDC, GCP Workload Identity, etc.) is the recommended approach. The pipeline authenticates via OIDC, receives an ephemeral certificate, signs the image, and the certificate expires — all without a persistent key to manage.

Signing in CI/CD

Here's a practical GitHub Actions workflow that builds, signs, and verifies a container image:

name: Build and Sign
on:
  push:
    branches: [main]

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

jobs:
  build-sign:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@93ea575cb5d8a053eeb0ac9dc73d9c01f5f5d2c3

      - uses: sigstore/cosign-installer@v2

      - name: Build and push
        run: |
          docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

      - name: Sign image
        run: |
          cosign sign ghcr.io/${{ github.repository }}:${{ github.sha }}
        env:
          COSIGN_EXPERIMENTAL: 1

The id-token: write permission allows the workflow to request an OIDC token from GitHub, which Fulcio uses to issue the signing certificate. The resulting signature is stored in the container registry alongside the image.

Where Signatures Live

Cosign stores signatures as OCI artifacts in the container registry, alongside the image they sign. For an image at registry.example.com/myapp@sha256:abc123, the signature is stored at a predictable tag based on the image digest.

This means:

  • No separate infrastructure needed for signature storage
  • Signatures travel with images when they're mirrored or transferred
  • Existing registry access controls apply to signatures
  • Registry garbage collection must be configured to not delete signature artifacts

Verification at Deployment

Signing is only useful if you verify. The most effective verification point is at deployment time, using a Kubernetes admission controller.

Sigstore Policy Controller

The Sigstore project provides a Kubernetes admission controller that verifies Cosign signatures:

apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: require-signed-images
spec:
  images:
    - glob: "ghcr.io/myorg/**"
  authorities:
    - keyless:
        identities:
          - issuer: https://token.actions.githubusercontent.com
            subject: https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main

This policy says: for any image from ghcr.io/myorg/, verify that it was signed by the specific GitHub Actions workflow in the specified repository and branch. If the signature doesn't match, the deployment is rejected.

Kyverno and OPA Gatekeeper

If you're already using Kyverno or OPA Gatekeeper for Kubernetes policy enforcement, both support Cosign signature verification as part of their policy engines.

Attestations: Beyond Signatures

Cosign doesn't just sign images — it supports attaching arbitrary attestations. Attestations are signed statements about the image, such as:

  • SBOM attestation: An SBOM describing the image's contents, signed by the build system
  • Vulnerability scan attestation: Results of a vulnerability scan, signed by the scanning tool
  • SLSA provenance attestation: A provenance statement describing how the image was built
# Attach a signed SBOM
cosign attest --predicate sbom.json --type spdxjson registry.example.com/myapp:v1.0

# Verify and retrieve the attestation
cosign verify-attestation --type spdxjson registry.example.com/myapp:v1.0

Attestations enable rich policy decisions: not just "was this image signed?" but "does this image have a current vulnerability scan with no critical findings?"

Common Pitfalls

Signing by tag instead of digest. Tags are mutable — always sign and verify by digest (@sha256:...), not by tag (:latest, :v1.0). Cosign handles this correctly by default, but your pipeline needs to track digests, not just tags.

Not verifying in production. Many organizations implement signing but skip verification, treating it as a checkbox exercise. Without verification, signing provides no security benefit.

Overly broad verification policies. A policy that accepts any signature from any identity provides minimal value. Verification should constrain the allowed signers to specific identities and OIDC issuers.

Forgetting about base images. Your application image is built on a base image. If the base image isn't signed, you have a gap in your chain of trust. Use signed base images and verify their signatures in your build process.

Registry compatibility. Not all registries support OCI artifacts for signature storage. Verify that your registry supports the OCI distribution spec before implementing Cosign.

How Safeguard.sh Helps

Safeguard.sh integrates container image signing and verification into its comprehensive supply chain security platform. Our tools automate Cosign signature verification across your container fleet, enforce signing policies through your deployment pipeline, and correlate image signatures with SBOMs and vulnerability data. With Safeguard.sh, container image signing becomes part of a unified supply chain security posture rather than an isolated control — giving you confidence that every deployed container is authentic, scanned, and policy-compliant.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.