By the end of this you will have a container image signed with Cosign v3, a passing verification both with a key pair and keyless from GitHub Actions, and the two fallback flags that keep you working when your registry or admission controller hasn't caught up to the v3 defaults.
Here is the thing the release notes bury. Cosign v3 shipped on October 8, 2025 (per the Sigstore blog), and the upgrade flips three opt-in flags to on-by-default at the same time: --new-bundle-format, --trusted-root, and --use-signing-config. The pitch is "fewer flags, one standardized format." The cost is that your signatures now land in your registry in a shape older tooling cannot see, and a cosign verify call that passed last quarter can fail, or hang, against an air-gapped registry.
This walks the full sign-and-verify loop and foregrounds the version-specific breakage you only learn by hitting it: Harbor not detecting the new bundle, verify reaching out to the TUF CDN even when you handed it a local key, and --tlog-upload=false turning into a hard error instead of a quiet no-op. It is for platform and supply-chain engineers already running admission-time image verification who are moving up from v2.x.
Prerequisites
cosignv3.0.4 or later. Check withcosign version. The v3.0.x line through v3.1.1 is where the default flip and the early breakage fixes live.- Docker or any OCI client, plus push access to a registry. To exercise the new format end to end, your registry must support OCI 1.1 referrers. GHCR and recent registries do; Harbor needs 2.15.0 (see Common pitfalls).
- For the keyless path: a GitHub Actions runner with
id-token: writepermission. No long-lived keys. - An image you can push, referenced by digest. Signing a mutable tag signs whatever the tag happened to point at, which is rarely what you mean.
Step-by-step
1. Confirm your version and the default that changed
cosign version
# cosign: v3.0.4 (or later)
In v3 the new bundle format is on by default. The Sigstore blog states --new-bundle-format, --trusted-root, and --use-signing-config all moved from opt-in to default-on. You no longer pass them. You now pass their negation when something downstream breaks.
2. Generate a key pair (skip if you go keyless)
cosign generate-key-pair
# writes cosign.key (encrypted private) and cosign.pub
Simplest path for a private registry. For CI, prefer keyless (Step 5) so there is no private key sitting somewhere to leak.
3. Resolve the digest, then sign by digest
IMG=ghcr.io/yourorg/app
DIGEST=$(docker buildx imagetools inspect "$IMG:latest" --format '{{.Manifest.Digest}}')
cosign sign --key cosign.key "$IMG@$DIGEST"
On v3 this writes a signature in the standardized bundle format (media type vnd.dev.sigstore.bundle.v0.3+json, per the Harbor issue thread) and stores it as an OCI 1.1 referring artifact instead of the legacy sha256-<digest>.sig tag. That storage change is the root of most upgrade surprises. Nothing about your signing command looks different. Where the signature lands does.
4. Verify with the public key
cosign verify --key cosign.pub "$IMG@$DIGEST" | jq .
A clean run prints the verified bundle JSON. If this hangs or errors on the network even though you supplied a local key, jump to Common pitfalls: v3.0.2 still tried to reach the TUF CDN in air-gapped setups (sigstore/cosign issue #4550).
5. Sign keyless from GitHub Actions
permissions:
id-token: write # required: mints the OIDC token Fulcio trusts
packages: write
steps:
- uses: sigstore/cosign-installer@v3
- run: cosign sign --yes "ghcr.io/${{ github.repository }}@${DIGEST}"
No --key. Cosign exchanges the workflow's OIDC token for a short-lived Fulcio certificate and records the signature in Rekor. There is no private key to steal because the certificate is ephemeral. This is the path I'd push any team toward for release artifacts.
6. Verify keyless by identity
cosign verify \
"$IMG@$DIGEST" \
--certificate-identity="https://github.com/yourorg/app/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" | jq .
Per the Cosign verification docs, one of --certificate-identity or --certificate-identity-regexp is mandatory for keyless flows, alongside --certificate-oidc-issuer. Pin the exact workflow path and ref. A regexp that matches any workflow in the org defeats the entire point of identity-based verification.
7. Wire verification into admission (Kyverno or policy-controller)
Keep enforcing at the cluster edge, but confirm your policy engine speaks the new bundle format before you flip the default fleet-wide. Test one namespace first:
kubectl run probe --image="$IMG@$DIGEST" -n verify-test
If the pod is rejected with a signature-not-found error while cosign verify on the CLI passes, your controller is still hunting for the legacy .sig tag. See Common pitfalls.
Verify it works
CLI verification should exit 0 and print a JSON bundle carrying the certificate subject and the Rekor entry. Then confirm independently that the signature actually exists as a referrer in the registry:
cosign tree "$IMG@$DIGEST"
# lists the attached signature/attestation artifacts
If cosign verify passes but your registry UI shows no signature, that is the OCI 1.1 referrer visibility gap, not a failed signature. The signature is there. The registry just can't render it yet.
Common pitfalls
- Harbor (and other registries) do not show the signature. Signatures created with the new bundle format carry media type
vnd.dev.sigstore.bundle.v0.3+json, which Harbor's signature detection did not recognize. goharbor/harbor issue #22401 targets the fix for Harbor 2.15.0. Until your registry supports it, sign with the legacy layout:cosign sign --new-bundle-format=false --key cosign.key "$IMG@$DIGEST". - Air-gapped verify still phones home. In sigstore/cosign issue #4550, v3.0.2 kept trying to reach the TUF CDN even with a local key on a Nexus-only network. The working offline shape is
cosign verify --key cosign.pub --offline --new-bundle-format=false --trusted-root trusted_root.json --local-image <dir>. Offline validation of the new protobuf bundle had not landed in an early v3 release, so--new-bundle-format=falseis the reliable disconnected path today. --tlog-upload=falsenow errors instead of silently skipping. Per the v3.0.4 release notes, disabling transparency-log upload is no longer allowed when--use-signing-config(now default) is set. Cosign fails before it writes the bundle. To sign without Rekor, also pass--use-signing-config=false.--bundleis now required where it was optional. The flag that names the output bundle file moved from optional to required in v3, so any script that omitted it will error on the bump.- CI cache signing regressed. moby/buildkit issue #6737 reports GitHub Actions cache signing breaking since cosign 3.0.4. If your build cache step fails right after the upgrade, pin the installer to a known-good version rather than chasing it live in CI.
- Signing a tag, not a digest. Always resolve and sign
@sha256:.... A tag is mutable, and you will eventually verify a different image than the one you signed. This one bites quietly, months later.
Wrap-up
You have a Cosign v3 signing and verification loop that works with a key pair and keyless from GitHub Actions, plus a clear read on the three behaviors that quietly changed underneath you: the new bundle format, OCI 1.1 referrer storage, and signing-config-driven Rekor upload. The two levers worth memorizing are --new-bundle-format=false and --use-signing-config=false, which keep you signing while Harbor, your air-gapped registry, and your admission controller catch up to the defaults.
Do this before you enforce anywhere: confirm your policy engine (Kyverno or Sigstore policy-controller) verifies the new bundle format in a test namespace, and version-pin cosign-installer in CI so a point release can't silently change your supply-chain guarantees overnight.

Comments
Be the first to comment.