Skip to main content

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 from keys_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 /license and confirm tier, expires_at, and entitlements are 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):

  1. Roll back the binary using the upgrade rollback procedure.
  2. The old key remains in the previous binary's TrustedKeys, so existing blobs continue to verify.
  3. Fix the TrustedKeys config, rebuild, and try again.