Key Rotation
What this page is
Runbooks for the four key types operators rotate in production: static API keys, encryption passphrases, node identity keys, and capability issuer keys.
Spec reference: Spec-10-Hardening key rotation, Spec-06-Capability-Tokens max token TTL, encryption key derivation in storage hardening, and Spec-05-Federation-Trust federation keys.
The four key types
Dual-trust window: ≥ 90 days for Ed25519 keys.
Both Ed25519 key types use a dual-trust window (Spec-10-Hardening): the retiring key remains in the accept-set for at least 90 days — long enough to cover all in-flight tokens (max TTL is 90 days per Spec-06-Capability-Tokens token shape).
Rotating static API keys
Static API keys are caller-generated bearer tokens stored only as
Argon2id hashes. Newly registered static keys get an enforced expiry
from STIGMEM_API_KEY_MAX_AGE_DAYS (default 90). The node rejects
expires_at values beyond that max-age. Operators can set the value
to 0 to disable enforcement for development, but production nodes
should keep a finite max-age.
Step 1 · Find keys nearing expiry
curl -s \
-H "Authorization: Bearer $STIGMEM_ADMIN_KEY" \
"https://your-node.example.com/v1/auth/keys/expiring-soon?within_days=30" \
| jq .
The response includes key ids, owners, permissions, tenant ids,
expiry timestamps, and days_remaining. It never includes raw key
material.
Step 2 · Register the replacement key
NEW_KEY="$(openssl rand -hex 32)"
curl -s -X POST \
-H "Authorization: Bearer $STIGMEM_ADMIN_KEY" \
-H "Content-Type: application/json" \
https://your-node.example.com/v1/auth/keys \
-d '{
"raw_key": "'"$NEW_KEY"'",
"entity_uri": "agent:ci-bot",
"permissions": ["read", "write"],
"description": "CI bot replacement key"
}' | jq .
Store NEW_KEY in your secrets manager immediately. The node returns
only metadata and never echoes the raw key.
Step 3 · Deploy and verify
Update the caller's secret, redeploy, then confirm the caller resolves to the expected identity:
curl -s \
-H "Authorization: Bearer $NEW_KEY" \
https://your-node.example.com/v1/me | jq .
Step 4 · Revoke the retired key
After the caller is healthy on the replacement key, revoke the old key id:
curl -s -X DELETE \
-H "Authorization: Bearer $NEW_KEY" \
https://your-node.example.com/v1/auth/keys/<old-key-id>
Multiple active keys may share one entity_uri during rotation. Use
distinct entity_uri values for distinct agents or tools; use key
descriptions for owner/ticket/deployment context.
Rotating the encryption passphrase
At-rest re-encryption requires exclusive database access. Stop the node before rekeying.
Prerequisites
stigmem-node[encryption,sqlcipher]
Installed.
Old + new passphrases
In separate environment variables.
Procedure
# 1. Stop the node
docker compose stop node # Docker Compose
# 2. Export the old and new passphrases into env vars
export OLD_PASSPHRASE_VAR=STIGMEM_OLD_KEY
export NEW_PASSPHRASE_VAR=STIGMEM_NEW_KEY
export STIGMEM_OLD_KEY="old-strong-passphrase"
export STIGMEM_NEW_KEY="new-strong-passphrase"
# 3. Run rekey
stigmem db rekey \
--old-passphrase-env OLD_PASSPHRASE_VAR \
--new-passphrase-env NEW_PASSPHRASE_VAR
# Expected output:
# → Rekeying stigmem.db ... done.
# → WAL checkpoint complete.
# → VACUUM complete.
# 4. Update your secrets manager with the new passphrase
# (AWS Secrets Manager, Vault, Doppler, etc.)
# 5. Update the env var name in STIGMEM_AT_REST_KEY_PASSPHRASE_ENV if it changed
# and remove the old passphrase from secrets
# 6. Restart the node
docker compose start node
# 7. Verify
curl -s https://your-node.example.com/healthz
# → {"status":"ok","backend":"sqlite"}
If you lose the passphrase, the database file is irrecoverable. Store
it only in a secrets manager — never in docker-compose.yml or
version control.
Rotating the federation keypair
Rotating the federation keypair changes your node's cryptographic identity. All peers that have pinned your current public key will stop trusting pull responses signed by the new key — you must coordinate the rotation with each peer operator.
Step 1 · Generate a new keypair
python3 -c "
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
import base64
priv = Ed25519PrivateKey.generate()
priv_bytes = priv.private_bytes_raw()
pub_bytes = priv.public_key().public_bytes_raw()
print('NEW_STIGMEM_FEDERATION_PRIVKEY=' + base64.urlsafe_b64encode(priv_bytes).decode())
print('NEW_STIGMEM_FEDERATION_PUBKEY=' + base64.urlsafe_b64encode(pub_bytes).decode())
"
Step 2 · Announce the rotation to peer operators
Share your new public key with each operator who has your node pinned. They must update the pin after you restart with the new key. Coordinate a maintenance window if you have many peers.
Step 3 · Update your secrets and restart
# Docker Compose — edit deploy/compose/.env (set STIGMEM_FEDERATION_PUBKEY and
# STIGMEM_FEDERATION_PRIVKEY to the new values), then restart the node:
docker compose up -d node
Step 4 · Verify new key is live
curl -s https://your-node.example.com/.well-known/stigmem | jq .public_key
# → "<new-base64url-pub>"
Step 5 · Ask peer operators to re-pin
Each peer operator runs:
# On each peer that had your old key pinned:
NEW_KEY=$(curl -s https://your-node.example.com/.well-known/stigmem | jq -r .public_key)
curl -X PATCH https://their-node.example.com/v1/federation/peers/<your-peer-id> \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d "{\"trusted_public_key\": \"$NEW_KEY\"}"
Until they update the pin, pull attempts from their node to yours
will fail with signature_mismatch in their audit log.
Step 6 · Update snapshot signing
Snapshots taken after the rotation are signed with the new key. Snapshots taken before the rotation can only be verified with the old public key — keep the old public key on record for snapshot archival verification.
# Verify an old snapshot with the old public key
stigmem snapshot verify /backups/old-snap.tar.gz --trusted-key <old-base64url-pubkey>
CLI-based rotation (Spec-10-Hardening)
The stigmem identity rotate-key command handles key generation,
dual-trust window setup, and transparency-log entries in a single
step:
# Dry-run first — shows new key_id and dual-trust expiry without committing
stigmem identity rotate-key --kind node --dry-run
# Commit the rotation
stigmem identity rotate-key --kind node
# Output:
# old key_id : a1b2c3d4e5f60001
# new key_id : 9f8e7d6c5b4a0002
# dual-trust : 2026-08-02T12:00:00Z
# ACTION REQUIRED — update your secrets manager with the new private key
# Rotate the capability issuer key
stigmem identity rotate-key --kind issuer
# Extend dual-trust window beyond the 90-day minimum
stigmem identity rotate-key --kind node --dual-trust-days 120
Each rotation writes a RotationEvent to the org manifest (signed by
the retiring key, anchoring trust to the prior identity) and two
transparency-log entries: the updated manifest and a
KeyRotationLogEntry.
After committing, update your secrets manager and restart the node —
the same steps as the manual procedure in
Step 3 above. Peers
refresh manifests automatically during their
refresh_peer_manifests() sweep.
Threat model
capability revoke; rotate again.verify_rotation_chain rejects any new_key_id already seen in the chain.trust_mode=strict, TL failure surfaces as a hard error; use --dry-run to pre-check.Rotation checklist
Encryption passphrase rotation
- Node stopped.
stigmem db rekeyran successfully.- New passphrase stored in secrets manager.
- Old passphrase removed from secrets manager.
- Node restarted and healthy.
- Health check passes.
Federation keypair rotation
- Static API keys checked for expiry via
/v1/auth/keys/expiring-soon. - Replacement static keys generated by callers and stored in secrets manager.
- Callers redeployed and verified with
/v1/me. - Retired static keys revoked.
- New keypair generated (manual or
stigmem identity rotate-key). - New key shared with all peer operators.
- Node secrets updated.
- Node restarted.
- New key visible at
/.well-known/stigmemor/.well-known/stigmem-manifest.json. - All peers updated their pins (or confirmed auto-refresh).
- Pull replication confirmed healthy on all peers.
- Old public key archived for snapshot verification.
- Dual-trust expiry date recorded in key rotation log.
- Reminder set to delete retiring key after
dual_trust_expires_at.