Software supply chain security has become a critical concern as organizations increasingly rely on container images as their primary deployment artifact. A container image pulled from a registry could have been tampered with in transit, built from compromised source code, or replaced entirely by an attacker who gained registry access. Without cryptographic verification, you’re deploying code on trust alone. Container image signing solves this by attaching a digital signature to each image, allowing consumers to verify that the image was produced by a trusted builder and hasn’t been modified since.
Cosign, part of the Sigstore project, is the leading tool for container image signing and verification. Its breakthrough innovation is “keyless” signing: instead of managing long-lived private keys (which themselves become targets), Cosign uses ephemeral keys backed by OIDC identity providers. A developer or CI system authenticates with GitHub, Google, or another OIDC provider, receives a short-lived certificate from Sigstore’s Fulcio CA, signs the image, and the signing event is recorded in Sigstore’s Rekor transparency log. The private key exists only in memory for the duration of the signing operation and is then discarded.
In a SaaS platform like the platform, Cosign is integrated into the CI/CD pipeline to sign every container image built from the main branch. GitHub Actions authenticates via OIDC to obtain a Fulcio certificate, signs the image digest (never the mutable tag), and verification is enforced at deployment time using admission controllers. This creates an unbroken chain of trust from source code commit to running container.
Sigstore is an umbrella project providing three key services: Cosign for signing and verifying container images and other artifacts, Fulcio as a free certificate authority that issues short-lived certificates based on OIDC identity, and Rekor as an immutable transparency log that records signing events. Together, they eliminate the need for key management while providing non-repudiation through the transparency log.
Keyless signing doesn’t mean “no keys.” It means the signing key is ephemeral—generated on the fly, used once, and discarded. Trust is anchored in the OIDC identity (e.g., a GitHub Actions workflow identity) and recorded in a tamper-proof transparency log, not in a long-lived private key that could be stolen.
Container image tags (like :latest or :v1.2.3) are mutable pointers—they can be moved to reference a different image at any time. Image digests (sha256:abc123...) are content-addressable and immutable. Cosign always signs the digest, never the tag. This ensures that the signature is bound to the exact image content. If the tag is moved to a different image, verification fails because the digest doesn’t match.
Cosign stores signatures as OCI artifacts in the same container registry as the image itself. The signature is pushed as a separate tag (derived from the image digest) in the same repository. This means no additional infrastructure is needed to store or distribute signatures—they travel with the image through any standard OCI registry.
Rekor records every signing event in an append-only Merkle tree. Even if an attacker compromises a signing identity, the transparency log provides a public audit trail. Any signature not present in the log can be rejected, and anomalous signing activity (unexpected signers, unusual times) can be detected by monitoring the log.
# .github/workflows/build-sign.yml
name: Build and Sign Container Image
on:
push:
branches: [main]
permissions:
contents: read
packages: write
id-token: write # Required for OIDC keyless signing
jobs:
build-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Sign the image
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
env:
COSIGN_EXPERIMENTAL: "true"
package verify
import (
"context"
"fmt"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/v2/pkg/cosign"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
"github.com/google/go-containerregistry/pkg/name"
)
// VerifyImage checks that a container image has a valid Cosign signature
// from the expected OIDC issuer and subject.
func VerifyImage(ctx context.Context, imageRef string, expectedIssuer string, expectedSubject string) error {
ref, err := name.ParseReference(imageRef)
if err != nil {
return fmt.Errorf("parse image ref: %w", err)
}
co := &cosign.CheckOpts{
RekorClient: rekor.GetRekorClient(rekor.DefaultRekorURL),
RootCerts: fulcio.GetRoots(),
CertOidcIssuer: expectedIssuer,
CertIdentity: expectedSubject,
ClaimVerifier: cosign.SimpleClaimVerifier,
}
signatures, _, err := cosign.VerifyImageSignatures(ctx, ref, co)
if err != nil {
return fmt.Errorf("signature verification failed: %w", err)
}
if len(signatures) == 0 {
return fmt.Errorf("no valid signatures found for %s", imageRef)
}
return nil
}
When verifying keyless signatures, you specify who should have signed (e.g., the GitHub Actions identity for your repo) rather than what key signed. This is more meaningful—you trust the CI pipeline identity, not a specific key that could be rotated or compromised.
The platform’s deployment pipeline enforces image signature verification at multiple stages. In the CI pipeline, after building and signing the image, a verification step immediately confirms the signature is valid and recorded in Rekor. At deployment time, a Kubernetes admission webhook (using the Connaisseur or Kyverno policy engine) intercepts pod creation requests and verifies that every image has a valid Cosign signature from the expected GitHub Actions workflow identity.
This prevents several attack vectors: a compromised registry cannot serve unsigned or re-signed images; a compromised CI pipeline on a non-main branch cannot produce images that pass verification (the OIDC subject includes the branch name); and any manual image push that bypasses CI will lack a valid signature and be rejected at deployment.
Beyond simple signatures, Cosign supports attaching attestations to images. The platform generates an SBOM (Software Bill of Materials) during the build process and attaches it as an in-toto attestation signed with the same keyless flow. This allows consumers to inspect the exact dependencies included in the image, supporting vulnerability scanning and compliance requirements.
Image signing is one layer of supply chain security. Combine it with pinned base images, reproducible builds, SBOM generation, vulnerability scanning, and admission controllers for comprehensive protection. Each layer catches what others miss.
:latest, the tag can be moved to a different (unsigned) image. Always sign by digest (@sha256:...) and verify by digest.id-token: write permission to request an OIDC token. Without this, Cosign falls back to interactive authentication, which fails in CI. This is the most common cause of “signing failed” errors in pipelines.--insecure-ignore-tlog) removes the non-repudiation guarantee. Only skip this in air-gapped environments where you operate your own Rekor instance.