DevSecOps

How to Rotate Build Signing Keys Safely

A step-by-step tutorial for rotating Cosign and GPG build signing keys without breaking existing attestations, verification chains, or downstream consumers.

Nayan Dey
Senior Security Engineer
6 min read

Rotating a build signing key is the sort of operation nobody wants to do until they absolutely have to, and by then it is usually under time pressure from an incident. The safe way is to rotate on a schedule with a deliberate overlap period, so every downstream consumer has time to pick up the new key before the old one stops signing. This tutorial walks through rotating a Cosign key pair used in CI and a GPG key used for release artifacts, keeping the old key available for verification while the new key takes over for signing. You will finish with a documented cutover and a revocation plan.

Prerequisites: cosign 2.2.0+, gpg 2.4+, a key store such as AWS KMS or HashiCorp Vault, and admin access to your CI system (GitHub Actions, GitLab CI, or similar). Time to complete: About 90 minutes spread over a 2-week overlap window.

Why not just replace the old key in place?

Replacing in place breaks every consumer that verifies against the old public key, and every existing signature becomes unverifiable. Rotation is a process, not an event: generate the new key, publish its public half, update signing to use the new key while continuing to serve the old public key for verification, wait out the overlap, then revoke the old key. Each phase has a clear entry and exit condition, which is what lets you recover if something goes wrong mid-rotation.

The same pattern applies regardless of the backing store. Whether your current key is a Cosign key in Vault, a KMS key in AWS, or an offline GPG key on a hardware token, the rotation shape is identical.

How do I generate the replacement Cosign key?

Generate the new key in your KMS so the private half never lands on disk. Assuming AWS KMS:

aws kms create-key \
  --description "cosign-signer-2024Q4" \
  --key-spec ECC_NIST_P256 \
  --key-usage SIGN_VERIFY \
  --tags TagKey=Purpose,TagValue=cosign \
         TagKey=RotationEpoch,TagValue=2024Q4

aws kms create-alias \
  --alias-name alias/cosign-signer-v2 \
  --target-key-id <new-key-id>

Export the public half into a file you can publish alongside the old one:

cosign public-key --key awskms:///alias/cosign-signer-v2 > cosign-v2.pub

Commit cosign-v2.pub to your public keys repo alongside the existing cosign-v1.pub. Downstream consumers should fetch both and try each in turn during verification. That is what turns the cutover into a no-op for them.

How do I run an overlap period where both keys sign?

For a typical two-week overlap, have CI produce signatures with both the old and new keys on every build. Cosign supports this natively with multiple --key flags:

IMG="ghcr.io/acme/api@${DIGEST}"
cosign sign --key awskms:///alias/cosign-signer-v1 -y "${IMG}"
cosign sign --key awskms:///alias/cosign-signer-v2 -y "${IMG}"

Each signature is pushed to the registry as a separate tagged artifact under the image's digest. Consumers verifying against either key succeed. During overlap, the release dashboard should display which keys produced signatures on each release so drift is visible:

cosign tree "${IMG}"

The expected output lists two signatures under the image digest, one per key. If you see only one, something in CI is skipping a step and needs to be fixed before cutover.

How do I cut over signing to the new key?

After the overlap window, remove the old cosign sign line from CI and leave only the new key. Cut over during a low-traffic window, and tag the first post-cutover release explicitly:

git tag -a v2.5.0 -m "First release signed exclusively with cosign-signer-v2"
git push origin v2.5.0

Monitor verification errors from downstream consumers for 72 hours. The main failure mode is a consumer who pinned to the old key explicitly rather than fetching from the keys repo. For those, either contact them directly or extend the overlap. Do not revoke the old key while any known consumer still references it by content.

How do I rotate a GPG release key the same way?

The GPG flow is similar but uses subkey rotation rather than a full new primary key. Create a new signing subkey on your existing primary:

gpg --expert --edit-key releases@acme.io
# In gpg prompt:
addkey
# Choose (10) ECC sign-only, Curve 25519, 2 years
save

Publish the updated public key to your keyserver or to the repo alongside the previous version:

gpg --export --armor releases@acme.io > acme-releases.asc
git add keys/acme-releases.asc
git commit -m "rotate: add 2024Q4 release signing subkey"

Tell gpg to prefer the new subkey when signing by expiring the old one rather than revoking it. Expired subkeys still verify past signatures, which is exactly what you want.

gpg --edit-key releases@acme.io
# Select the old signing subkey (key 1)
key 1
expire
# Set expiration to today
save

For the next release, gpg --armor --detach-sign release.tar.gz automatically uses the new subkey. Existing signatures on past releases continue to verify because the expired subkey is still trusted for its validity period.

How do I revoke the old key when I am ready?

Generate a revocation certificate first, keep it offline, and publish only when you are certain no consumer still needs the old key:

gpg --output revoke-v1.asc --gen-revoke releases@acme.io
# For Cosign, schedule deletion in KMS:
aws kms schedule-key-deletion \
  --key-id <old-key-id> \
  --pending-window-in-days 30

The 30-day KMS pending window is the last safety net. If a consumer reports a verification failure during that window, cancel the deletion:

aws kms cancel-key-deletion --key-id <old-key-id>

For GPG, publish the revocation certificate to your keyserver and keys repo, then update documentation to list the old key as revoked as of the current date. Keep the revocation certificate itself in your secure vault in case you need to reissue.

How do I document the rotation for auditors?

Produce a short rotation record in your runbook repo. Each rotation should list the old key ID and fingerprint, new key ID and fingerprint, overlap window dates, cutover tag or release, and revocation date. Commit this with a signed commit so the record itself carries a verifiable trail:

rotation-2024Q4.md
- Old key: awskms alias/cosign-signer-v1, fingerprint SHA256:ab12...
- New key: awskms alias/cosign-signer-v2, fingerprint SHA256:cd34...
- Overlap: 2024-10-08 to 2024-10-22
- First cutover release: v2.5.0 (2024-10-23)
- Old key scheduled for revocation: 2024-11-22

Schedule the next rotation on the calendar before closing out the current one. A signing key with no rotation date in the calendar drifts into permanent use, which is exactly what rotation is supposed to prevent.

How Safeguard Helps

Safeguard tracks signing keys as first-class supply chain assets and reports which of your products still reference a rotating or revoked key across their attestation chain. Griffin AI correlates key usage with SBOMs so you can see which images and artifacts need re-signing after a rotation. Policy gates block releases where a key has passed its expiration window or where attestations reference a revoked identity, catching pipelines that missed the cutover. The rotation record becomes part of your compliance evidence pack, ready for auditors without having to reconstruct it after the fact.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.