Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLIII · Container CIContainer CI

Image signing in CI

Intermediate⏱ ~26 mingitdocker

What you'll learn

  • Sign a container image with cosign using a keypair
  • Sign a container image keylessly with cosign and an OIDC token from CI
  • Attach an SBOM attestation to a signed image
  • Apply the production discipline around key rotation, transparency log, and verification policy

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A signature on an image is what closes the supply-chain loop opened in LIII-01. The signature is the attestation that binds the artifact to the build - the statement, signed by something the verifier trusts, that “this digest was produced by this build, from this source, on this runner”. Without it, every tag in the registry is a guess. With it, every tag is a verifiable claim.

cosign and the Sigstore stack

Cosign is the signing tool of the Sigstore project. It signs OCI artifacts (images, SBOMs, attestations, anything that the registry stores) using public-key cryptography. The verification side is a verifier checking the signed claim against a public key or, for keyless mode, against the Sigstore certificate transparency log.

Three Sigstore components matter:

  • Fulcio issues short-lived X.509 certificates bound to an OIDC identity (a CI workflow’s OIDC token from GitHub Actions, GitLab CI, etc.). The certificate says “this signing key was controlled by this OIDC subject during this short time window”.
  • Rekor is an append-only transparency log of signed public statements. Every signature is recorded; the log is the tamper-evident record.
  • cosign is the CLI that creates the signature, attaches the attestation, and verifies both.
flowchart LR
    A["OIDC token (CI)"] --> B["Fulcio"]
    B --> C["Short-lived signing cert"]
    A --> D["cosign sign"]
    C --> D
    D --> E["Signature"]
    E --> F["Rekor (transparency log)"]
    E --> G["Registry"]
    G --> H["Verifier"]
    F --> H

The keyless flow is what most CI pipelines use. The CI provider (GitHub Actions, GitLab, CircleCI) issues an OIDC token that proves the workflow’s identity to Fulcio; Fulcio returns a short-lived signing certificate; cosign signs the digest with the private key derived for that window; Rekor records the signature; the registry stores both the signed signature and the payload.

Signing with a keypair

The classical flow is a long-lived keypair. The team holds a private key; the public key is published; cosign signs with the private key and verifies with the public key.

cosign generate-key-pair
cosign sign --key cosign.key ghcr.io/org/app:$COMMIT_SHA
cosign verify --key cosign.pub ghcr.io/org/app:$COMMIT_SHA

The advantages are completeness of control (the team holds the private key) and simplicity (no third party). The disadvantages are rotation, revocation, and the operational risk of the private key landing in unintended hands. A leaked signing key is a key the team must rotate immediately; rotation is operationally expensive because every deploy and verifier must be updated.

Signing keylessly

The keyless flow uses an OIDC token from CI as the authority. The private key never exists outside the CI runtime; Fulcio issues a certificate that names the OIDC subject (e.g., https://github.com/org/repo/.github/workflows/build.yml@refs/heads/main) and the signing happens with the per-run ephemeral key.

export COMMIT_SHA=$(git rev-parse --short HEAD)
cosign sign --keyless ghcr.io/org/app:$COMMIT_SHA

Under the hood, cosign reads the ACTIONS_ID_TOKEN_REQUEST_TOKEN and related environment variables from the GitHub Actions runner, exchanges them with Fulcio, and signs with the resulting short-lived key. The signature plus the Fulcio certificate plus the Rekor entry are stored as OCI attestations on the registry.

The advantages:

  • No long-lived key to leak. The signing material is the OIDC token, which is short-lived and tied to the specific CI workflow run.
  • Identity is bound to the workflow. The Fulcio certificate records which GitHub Actions workflow (or equivalent) signed.
  • Tamper-evident by default. The Rekor entry is the audit record.

The disadvantage is dependency on Sigstore infrastructure (Fulcio, Rekor). Sigstore’s public-good instance has an SLA but not an SLO; teams with strict availability requirements may run their own.

Attaching the SBOM as an attestation

The signature itself only attests “this digest is what the signer intended”. The full picture - “this digest was built from this source and contains these components” - requires an attestation. cosign attaches an SBOM as an attestation:

cosign attach sbom --sbom sbom.spdx.json \
    ghcr.io/org/app:$COMMIT_SHA
cosign sign --keyless ghcr.io/org/app:$COMMIT_SHA

The attach step stores the SBOM as an OCI artifact at a well-known tag (a “referrer” to the signed digest); the sign step produces the signature. Verifiers iterate the referrers, fetch the SBOM, and check its signature. Tools like cosign verify-attestation make this end-to-end.

In SLSA v1.2 vocabulary, the signature plus the SBOM plus the provenance attestation produces the “Build track” guarantee that SLSA calls Level 3: a tamper-resistant build, isolated from collaboration, that emits verifiable provenance.

Production discipline

  • Sign with the runner’s identity, not a developer’s. A signature from a developer’s laptop has no chain to anything. A signature from the CI runner with a pinned OIDC subject has the chain.
  • Pin the OIDC subject in the verifier. cosign verify with --certificate-identity and --certificate-identity-regexp ensures the signature was made by this workflow. Wildcards accepting https://github.com/org/* are weaker than pinning https://github.com/org/repo/.github/workflows/build.yml@refs/heads/main.
  • Verify before deploy, not after. Verification belongs in the admission controller and the CD pipeline. A signature that is only checked at audit time is a signature that did not stop the bad deploy.
  • Monitor Rekor inclusion. A signature that fails to land in Rekor has lost the tamper-evidence property. The CI job should fail if the Rekor inclusion proof is missing.
  • Rotate keys (or document the keyless choice). For keypair flows, document the rotation cadence. For keyless, document the dependency on Sigstore and the failover plan.

Production discipline (concrete checklist)

  1. Sign every image with cosign sign --keyless in the same job that builds and tests it.
  2. Pin the certificate identity in cosign verify to the workflow path.
  3. Attach the SBOM as an attestation before sign.
  4. Verify in the admission controller on every pull.
  5. Fail the CI job if Rekor inclusion fails.
  6. Document the key or keyless decision; review it quarterly.

Cross-course references

  • Container Security for Production Sysadmins - Parts VII-VIII (admission control and policy) cover the verifier side: kyverno and policy controllers that gate deploys on a verified signature.
  • Sigstore project - the broader Sigstore documentation covers the Fulcio and Rekor components in detail for teams that need to operate their own.

Quiz

Knowledge check · 4 questions

  1. Q1. Why is a signature without an enforced verification policy (an admission controller, a CI gate, a CD promotion rule) effectively meaningless?

  2. Q2. In a keyless cosign flow, the signing private key persists across CI runs and is stored in the runner's secrets store.

  3. Q3. Name the three Sigstore components that participate in a keyless cosign signing flow and what each one contributes.

  4. Q4. Diagnose why signed images are still being admitted to the cluster despite a signing policy in place.

    A team moves from keypair cosign signing to keyless with an OIDC subject pinned to https://github.com/org/repo/.github/workflows/build.yml@refs/heads/main. The CI signs every image with cosign sign --keyless. The admission controller uses cosign verify with --certificate-identity-regexp 'https://github.com/org/.*' and a star wildcard. Production pods still run unsigned images built on a developer's laptop.

Passing score: 75%. Answers are checked in this browser.