Master Encryption Key Rotation (Machine Identity KEK)
Runbook for rotating the site master encryption key used to protect per-org JWT signing private keys and token-delegation credentials at rest.
This is not the same as per-org JWT signing key rotation (see JWT Signing Key Rotation).
Design background: SPIFFE JWT-SVID SDD Β§3.1.1.
API surface: Re-wrap is available through NICo REST at POST /v2/org/{org}/nico/site/{siteID}/tenant-identity/re-encrypt for provider admins and directly through Core gRPC (ReencryptTenantIdentitySecrets) with a Forge Admin CLI mTLS certificate. There is no nico-admin-cli subcommand for re-wrap.
Concepts
Decrypt always loads the AES key named by the envelopeβs embedded key_id. The site current_encryption_key_id only selects which key is used when writing new ciphertext.
Fields re-wrapped per org (when present):
encrypted_signing_key_1encrypted_signing_key_2encrypted_auth_method_config(token delegation credentials)
Prerequisites
[machine_identity].enabled = trueand a healthynico-api.- Credentials for the selected surface: provider-admin bearer-token access for NICo REST, or a Forge Admin CLI mTLS client certificate for direct gRPC (see Generating client certificates).
- Maintenance window: plan for a config change + API restart + one re-encrypt pass. Issuance can continue during re-wrap if old keys remain in secrets.
Procedure
Example rotation: kv1 β kv2.
Step 1 β Add the new key to secrets
Generate material:
Add kv2 to site credentials without removing kv1:
For Vault-backed sites, add β¦/machine_identity/encryption_keys/kv2 using your standard secret workflow.
Deploy/refresh credentials so nico-api can read both keys.
Step 2 β Point site config at the new key and restart API
Update site config:
Restart nico-api (this setting is not hot-reloaded). New encrypts (new orgs, signing-key rotation, delegation updates) use kv2 immediately. Existing DB rows still decrypt with kv1 via envelope metadata.
Step 3 β Dry-run re-wrap
The following examples use direct gRPC. For NICo REST, use the JSON field names dryRun and organizationId:
- The URL
{org}identifies the provider. - A non-null
organizationIdidentifies the tenant by itsorgidentifier, not by its REST resource UUID or display name. The identifier must contain one or more ASCII letters, digits, underscores, or hyphens. NICo REST matches it case-insensitively, lowercasing it before the Tenant lookup and before forwarding it to Core. The tenant must have an allocation and tenant identity configuration on the selected Site. - Omit
organizationIdor set it to JSONnullto select all organizations. Empty and whitespace-containing strings return Bad Request instead of broadening the scope. - NICo REST returns Bad Request when the tenant is unknown or has no allocation on the selected Site. On either surface, a valid scoped tenant without tenant identity configuration returns Not Found.
- Direct gRPC trims
organization_idand treats an omitted or blank value as all organizations. It does not lowercase the value.
Call ReencryptTenantIdentitySecrets with dry_run: true. Optionally scope to one org.
Single org:
Inspect the response:
Success criterion for dry run: no failures; every row either skipped as already on target or listed for re-encryption.
Step 4 β Apply re-wrap
Repeat with dry_run: false:
Verify:
rows_failed = 0- After apply, a second dry run shows only
rows_skipped_all_on_target/fields_skipped_on_target
Step 5 β Retire the old key (optional)
After a successful apply and verification:
- Confirm no ciphertext still references
kv1(second dry run withcurrent_encryption_key_id = "kv2"should skip all rows). - Remove
kv1frommachine_identity.encryption_keysin secrets. - Redeploy credentials. Do not remove
kv1until re-wrap completes β decrypt would fail for any remainingkv1envelopes.
Rollback
If apply fails partway:
- Keep both keys in secrets.
- Leave
current_encryption_key_idatkv2(or revert tokv1only if you have not written new ciphertext withkv2yet). - Fix the reported
failures(often missing key material or corrupt envelope). - Re-run dry run, then apply.
If you must revert current_encryption_key_id to kv1 before re-wrap completes, new writes use kv1 again; rows already re-wrapped to kv2 still decrypt with kv2 until you re-run re-encrypt toward kv1.
Operational Notes
- Run re-wrap after every change to
current_encryption_key_id. - Re-wrap is idempotent: rows already on the target key are skipped.
- Org-scoped runs are useful for phased rollout; site-wide run (
organization_idomitted) is typical for small fleets. - Coordinate with JWT signing key rotation β they are independent, but both affect what verifiers and decrypt paths must tolerate.
Related Documentation
- Day 0 Machine Identity β initial KEK provisioning
- Machine Identity (Day 1) β per-org config and JWT signing-key rotation
- SPIFFE JWT-SVID SDD Β§3.1.1 β authoritative design