Verified9 commandsAI-assisted

cosign

.md

Verified 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#

Diagram

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 version
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 Vault

Verifying#

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.gz

Tip

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.gz

Useful 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 image

Real-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.com

An 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: write in 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.