Skip to main content
Version: v0.9.0a2
Operator

Key Rotation

5 min readOperator · Security engineerSpec-10-Hardening

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

Key type
Max cadence
Purpose · Rotation impact
Static API key
≤ 90 days by default
Bearer-token access for operators, agents, services, and federation admin flows. Caller secret must be redeployed; old key is revoked after overlap.
Encryption passphrase
At-rest encryption of the database file. Node must stop; no peer impact.
Node identity key (Ed25519)
≤ 365 days
Federation peer authentication, snapshot signing, manifest self-signature. All pinned peers must re-pin or auto-refresh.
Capability issuer key (Ed25519)
≤ 90 days
Signing capability tokens issued to subjects. In-flight tokens valid during dual-trust window.

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

Node must be stopped

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"}
Passphrase loss = data loss

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

Threat
Class
Mitigation
Stolen retiring key during window
key compromise
Revoke all tokens issued under that key via capability revoke; rotate again.
Forged rotation event
authenticity
Rotation event signature must verify under the retiring key.
Key reuse / regression attack
replay
verify_rotation_chain rejects any new_key_id already seen in the chain.
TL unavailable during rotation
witness
In trust_mode=strict, TL failure surfaces as a hard error; use --dry-run to pre-check.
Dual-trust window too short
policy
CLI enforces minimum 90 days; Spec-10-Hardening.2 prohibits shorter windows.

Rotation checklist

Encryption passphrase rotation

  1. Node stopped.
  2. stigmem db rekey ran successfully.
  3. New passphrase stored in secrets manager.
  4. Old passphrase removed from secrets manager.
  5. Node restarted and healthy.
  6. Health check passes.

Federation keypair rotation

  1. Static API keys checked for expiry via /v1/auth/keys/expiring-soon.
  2. Replacement static keys generated by callers and stored in secrets manager.
  3. Callers redeployed and verified with /v1/me.
  4. Retired static keys revoked.
  5. New keypair generated (manual or stigmem identity rotate-key).
  6. New key shared with all peer operators.
  7. Node secrets updated.
  8. Node restarted.
  9. New key visible at /.well-known/stigmem or /.well-known/stigmem-manifest.json.
  10. All peers updated their pins (or confirmed auto-refresh).
  11. Pull replication confirmed healthy on all peers.
  12. Old public key archived for snapshot verification.
  13. Dual-trust expiry date recorded in key rotation log.
  14. Reminder set to delete retiring key after dual_trust_expires_at.

See also