cosign
.mdVerified against cosign v3.1.3, flags verified via `cosign --help` / `cosign sign --help` / · official docs
What it is and where it fits 🎯#
cosign signs and verifies container images, blobs, and attestations (SBOMs, provenance) so a downstream consumer — an admission controller, a verification pipeline, a curious engineer — can prove an artifact came from where it claims to and hasn't been tampered with since. It's the last link in the supply-chain trust chain this series builds up: Syft says what's in an image, Grype/Trivy say whether that's vulnerable, and cosign says the whole thing is provably authentic. Supports both keyless (OIDC/Fulcio-based, no key management) and traditional key-pair signing.
Keyless signing — how the trust actually gets established#
No long-lived private key ever exists to leak — the signing key is generated fresh, used once, and the identity binding (a real CI pipeline's OIDC identity, not an anonymous keypair) is what a verifier actually checks against.
Installation#
curl -sL -o cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign && sudo mv cosign /usr/local/bin/
go install github.com/sigstore/cosign/v3/cmd/cosign@latest # if you have a Go toolchain (note the v3 module path)
brew install cosign
cosign versionKeyless signing (recommended — no key management)#
cosign sign myregistry.io/myapp:latest # opens a browser for the Sigstore OIDC flow, signs by digest
cosign sign --yes myregistry.io/myapp@sha256:abc123... # skip the confirmation prompt (for CI)Important
Always sign by digest (@sha256:...), not by tag. A tag can be repointed after signing, so signing a
tag doesn't guarantee what gets verified later is actually what you signed — cosign sign itself warns
about exactly this. In CI (GitHub Actions, GitLab CI), the ambient OIDC token from the platform is picked
up automatically — no interactive browser flow needed, which is what makes keyless signing practical in an
unattended pipeline at all.
Key-pair signing#
cosign generate-key-pair # writes cosign.key (private) + cosign.pub (public)
cosign sign --key cosign.key myregistry.io/myapp@sha256:abc123...
cosign sign --key env://COSIGN_PRIVATE_KEY myregistry.io/myapp@sha256:abc123... # key from an env var, not a file
cosign sign --key awskms://alias/my-signing-key myregistry.io/myapp@sha256:... # key held in AWS/GCP/Azure KMS or VaultVerifying#
cosign verify myregistry.io/myapp:latest \
--certificate-identity=you@example.com \
--certificate-oidc-issuer=https://accounts.google.com # keyless verification requires pinning expected identity + issuer
cosign verify --key cosign.pub myregistry.io/myapp:latest # key-pair verification
cosign verify myregistry.io/myapp:latest \
--certificate-identity-regexp='.*@example\.com$' \
--certificate-oidc-issuer-regexp='https://token\.actions\.githubusercontent\.com' # e.g. "signed by any GitHub Actions workflow in our org"Warning
Keyless verification without --certificate-identity/--certificate-oidc-issuer (or their regexp
variants) is rejected outright. You must state who/what you expect to have signed the artifact — otherwise
"verified" only proves someone went through the Sigstore flow, not that it was specifically your pipeline.
Skipping this check is the single most damaging mistake possible with keyless signing: it turns a strong
identity-bound guarantee into a meaningless "a signature exists" check.
Attestations (SBOM, provenance, scan results)#
cosign attest --predicate sbom.cdx.json --type cyclonedx --key cosign.key myapp@sha256:abc123...
cosign verify-attestation --type cyclonedx --key cosign.pub myapp:latest
cosign attest-blob --predicate provenance.json --key cosign.key artifact.tar.gzTip
One verification pipeline (cosign verify-attestation) checks any attestation type — SBOM, build
provenance, scan results — without needing format-specific parsing logic per attestation kind. Standardizing
on in-toto-style attestations for everything you want to attach to an artifact (rather than inventing a
bespoke format per data type) is what makes this generality actually pay off operationally.
Blob (non-container-artifact) signing#
cosign sign-blob --key cosign.key myfile.tar.gz --output-signature myfile.sig
cosign verify-blob --key cosign.pub --signature myfile.sig myfile.tar.gzUseful for signing something that isn't a container image at all — a Terraform module tarball, a release binary, a Helm chart package.
Inspecting what's attached to an image#
cosign tree myregistry.io/myapp:latest # show signatures, SBOMs, and attestations attached to an imageReal-world scenario: enforcing "only signed images from our CI" at admission time#
A cluster wants to reject any image that wasn't signed by the org's actual GitHub Actions pipeline — combining cosign's verification with a Kubernetes admission controller (Kyverno or Gatekeeper/OPA, from earlier in this series):
cosign verify myregistry.io/myapp:latest \
--certificate-identity-regexp='https://github.com/my-org/.*' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.comAn admission policy wraps this exact check as a Kubernetes-native gate — any pod spec referencing an unsigned, or wrongly-signed, image is rejected before it ever schedules, closing the loop between "we sign in CI" and "we actually enforce that signature means something in production."
Real-world scenario: GitHub Actions signing recipe#
# .github/workflows/sign.yml
name: Sign and push
on:
push:
tags: ['v*']
permissions:
id-token: write # required for keyless signing's ambient OIDC token
contents: read
packages: write
jobs:
build-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- name: Build and push
run: |
docker build -t ghcr.io/my-org/myapp:${{ github.ref_name }} .
docker push ghcr.io/my-org/myapp:${{ github.ref_name }}
- name: Sign
run: |
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/my-org/myapp:${{ github.ref_name }})
cosign sign --yes "$DIGEST"Important
permissions: id-token: write is not optional — without it, GitHub Actions never issues the OIDC token
keyless signing depends on, and the sign step fails outright rather than silently falling back to something
less secure.
Common pitfalls#
- Signing by tag instead of digest — see the IMPORTANT callout above.
- Verifying without pinning identity/issuer — see the WARNING above; this is the mistake that makes keyless verification meaningless.
- Forgetting
id-token: writein GitHub Actions — the most common reason keyless signing "doesn't work" in a fresh CI setup. - Assuming a signature alone proves the image is safe — cosign proves authenticity/provenance, not absence of vulnerabilities; it's complementary to Trivy/Grype, not a substitute for scanning.
Exit codes#
0 success · non-zero on a signing/verification failure — check stderr for the specific reason
(certificate mismatch, transparency-log lookup failure, missing key, etc.).
When to reach for something else#
cosign is the de facto standard for OCI-artifact signing in the Sigstore ecosystem; there isn't a direct substitute covered elsewhere in this series. It pairs with, rather than replaces, Syft (SBOM generation) and Trivy/Grype (vulnerability scanning) — signing an unscanned, vulnerable image just proves you know exactly which vulnerable image you shipped.