Runbook: License and Package Signing Key Rotation
When to use this
Use this runbook to rotate the Ed25519 key pair used to sign Alexandria EE license blobs and .atool packages. Rotate on an annual cadence, after a suspected key compromise, or when the key custodian changes.
For the full conceptual background on hot vs. cold rotation, see License Key Rotation.
Pre-checks
- Confirm you have access to the current private key (file path or CI secret).
- Confirm you have a secure destination for the new private key (password manager, HSM, encrypted storage).
- For hot rotation: verify that the binary build pipeline is green before starting, so the two-phase deploy does not leave customers stuck on an unverifiable binary.
Procedure
1. Generate a new Ed25519 keypair
alex-license rotate --keygen-only --kid-new <new-key-id>
# e.g.: alex-license rotate --keygen-only --kid-new prod-2026
Output includes kid, pub (public key), and priv (private key). Save priv immediately to secure storage. The pub value goes into the binary.
2. Add the new public key to TrustedKeys
For a dev/staging binary (no build-time injection):
// api-go/internal/license/keys_embedded.go
add("prod-2026", "<pub value>")
For the production binary (built with -tags prod), set in your CI secret manager:
ALEXANDRIA_PROD_TRUSTED_KID=prod-2026
ALEXANDRIA_PROD_TRUSTED_KEY_PEM=<pub value>
For a hot rotation, keep the old entry alongside the new one until all licenses are migrated.
3. Build and deploy binaries (hot rotation)
gcloud builds submit --config cloudbuild.yaml
Follow the upgrade runbook to deploy. Verify that old licenses still pass before proceeding.
4. Re-issue customer licenses under the new key
alex-license rotate \
--kid-old prod-2025 \
--kid-new prod-2026 \
--priv-old @/path/to/old.priv \
--priv-new @/path/to/new.priv \
--blob @/path/to/customer.blob \
> customer_new.blob
The payload (tier, seats, expiry, entitlements) is preserved. Distribute customer_new.blob to each customer — see license-renewal.md for the delivery procedure.
5. Re-sign .atool packages with the new key
For packages that were signed under the old key and need to carry the new signature going forward:
# Generate a standalone keypair for package signing (if separate from license key):
alexandria pkg sign --keygen-only
# Re-sign an existing archive in-place, verifying the old signature first:
alexandria pkg resign \
--priv-old /path/to/old.pkcs8 \
--priv-new /path/to/new.pkcs8 \
--archive /path/to/tool_1.0.0_amd64.atool
# Optional: write to a new output path instead of overwriting:
alexandria pkg resign \
--priv-old /path/to/old.pkcs8 \
--priv-new /path/to/new.pkcs8 \
--archive /path/to/tool_1.0.0_amd64.atool \
--output /path/to/tool_1.0.0_amd64_resigned.atool
6. Remove the old key (hot rotation only)
Once all customers confirm their new license blob verifies:
- Remove the old
add("prod-2025", …)line fromkeys_embedded.go(or clear the old CI secrets). - Rebuild and redeploy.
For cold rotation (compromise), step 6 is merged into step 2 — the old key is removed immediately.
Verification
- Deploy the new binary and load a customer's new license blob.
- Hit
/licenseand confirmtier,expires_at, andentitlementsare correct. - Verify a re-signed atool:
alexandria pkg verify --archive /path/to/tool.atool --require-signed
Rollback
If the new binary rejects existing license blobs (TrustedKeys misconfigured):
- Roll back the binary using the upgrade rollback procedure.
- The old key remains in the previous binary's TrustedKeys, so existing blobs continue to verify.
- Fix the TrustedKeys config, rebuild, and try again.