Signing artifacts in CI

Cosign keyless from the pipeline.

Advanced12 min · lesson 13 of 17

You scanned the image and it passed — but what proves the image running in production is the same one your pipeline built and scanned, and not one an attacker swapped in the registry? Signing answers that. The pipeline cryptographically signs the exact image digest it produced, and downstream (at deploy or admission) that signature is verified. Without the verify step signing is theatre, but together they make a tampered or unauthorized image detectable.

.gitlab-ci.yml
sign-image:
stage: sign
image: gcr.io/projectsigstore/cosign
id_tokens:
SIGSTORE_ID_TOKEN: { aud: sigstore } # GitLab OIDC identity for keyless
script:
- cosign sign --yes "$IMAGE@$DIGEST" # sign the digest, not a mutable tag
rules:
- if: '$CI_COMMIT_BRANCH == "main"'

Keyless signing: no key to leak

Traditional signing means a private key you must store and protect — and in CI, a stored signing key is just another leakable secret. Cosign keyless signing removes it entirely: it uses the pipeline’s OIDC identity to get a short-lived certificate from Fulcio, signs with that, and records the signature in the public Rekor transparency log. There is no long-lived key anywhere — the signature is bound to "GitLab CI, this project, this pipeline," which is exactly the identity you want to verify later.

Sign the digest, verify the identity

Two details make signing meaningful. Sign the immutable digest (image@sha256:...), never a tag — a tag can be repointed, so signing it proves nothing about the bytes. And when you verify, assert the identity you expect: not merely "is it signed" but "is it signed by our GitLab project’s CI identity." An attacker can always sign their own image with their own identity; the verification must pin who signed it.

terminal
$ cosign verify \
--certificate-identity-regexp "https://gitlab.acme.internal/acme/.*" \
--certificate-oidc-issuer https://gitlab.acme.internal \
"$IMAGE@$DIGEST" # non-zero exit = reject; identity must match, not just "signed"
A signature nobody verifies protects nothing
Signing in the pipeline is only half the control — if no one checks the signature before running the image, an unsigned or attacker-signed image sails through unchanged. The verification (at deploy time, or better at Kubernetes admission with a policy that pins your CI identity) is what actually enforces it. The advanced course and the Kubernetes security course cover that admission gate in depth.