Skip to content

Secrets rotation

Orimora’s secrets do two different jobs, and they rotate in two different ways:

  • Encryption keys (*_ENCRYPTION_KEY, AES-256-GCM) encrypt stored secrets — they are reversible, so rotation means re-encrypting the data with the new key (a script).
  • HMAC secrets (*_SECRET) hash tokens at rest — they are one-way, so rotation either uses a dual-key window or requires reissuing the affected tokens.
SecretProtectsRotation
IDENTITY_ENCRYPTION_KEYSSO provider configs/secretsRe-encrypt script
MFA_ENCRYPTION_KEYTOTP secretsRe-encrypt script
LLM_ENCRYPTION_KEYStored AI provider API keysRe-encrypt script
PUBLISHING_ENCRYPTION_KEYWebhook/publishing secrets, off-site configRe-encrypt script (+ re-save off-site)
API_KEY_SECRETREST API keys (long-lived)Dual-key window (API_KEY_SECRET_PREVIOUS)
SCIM_TOKEN_SECRETSCIM provisioning tokensReissue tokens
MAGIC_LINK_SECRETMagic links (15 min), invites, fallback rootBrief window; reissue invites if needed
SESSION_SECRETStep-up (10 min) + MFA-pending (5 min) cookiesRotate anytime — sessions are unaffected

Encryption keys are rotated with scripts/rotate-encryption-key.mjs, which decrypts every affected row with the old key and re-encrypts it with the new one, atomically. Each row is round-trip self-checked before anything is written, and any row that fails to decrypt with the old key aborts the whole run — there is no partial state.

  1. Generate a new 32-byte key:

    Terminal window
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
  2. Dry-run (reads + validates everything, writes nothing):

    Terminal window
    node scripts/rotate-encryption-key.mjs --key llm \
    --old "$OLD_KEY" --new "$NEW_KEY" --dry-run

    --key is one of identity, mfa, llm, publishing. It prints a per-table row count.

  3. Apply (single transaction):

    Terminal window
    node scripts/rotate-encryption-key.mjs --key llm --old "$OLD_KEY" --new "$NEW_KEY"
  4. Swap the env var to the new value (e.g. LLM_ENCRYPTION_KEY=$NEW_KEY) and restart the app.

  5. Verify the relevant feature works (e.g. open an AI/LLM config, or a webhook), then discard the old key.

The ciphertext format has no embedded key version, so rotation re-encrypts the full data set rather than lazily per row. A future enhancement (key id in the payload) could enable rolling rotation; until then, the script’s atomic full re-encrypt is the supported path.

Rotating API_KEY_SECRET (seamless, dual-key)

Section titled “Rotating API_KEY_SECRET (seamless, dual-key)”

REST API keys are HMAC-hashed and cannot be re-hashed (the plaintext token isn’t stored). To rotate without breaking integrations, run a dual-key window:

  1. Set the old value as API_KEY_SECRET_PREVIOUS and the new value as API_KEY_SECRET, then deploy. Existing keys keep validating (Orimora accepts either secret).
  2. Reissue API keys over your chosen window (users regenerate them under Settings → Developers). New keys are hashed under the new secret.
  3. Once old keys are retired, remove API_KEY_SECRET_PREVIOUS and redeploy. Any key still on the old secret stops working.
  • SCIM_TOKEN_SECRET — reissue the team’s SCIM token from the SSO/SCIM admin page and update your IdP. (Set it to an independent value first if it currently falls back to API_KEY_SECRET.)
  • MAGIC_LINK_SECRET — invalidates in-flight magic links (15 min) and pending invites. If it isn’t set independently, it is also the fallback for API_KEY_SECRET/SCIM_TOKEN_SECRET/EMAIL_CHANGE_SECRETset those independently before rotating it, or you will invalidate API keys too. Rotate during low traffic; reissue invites if needed.
  • SESSION_SECRET — only signs short-lived step-up (10 min) and MFA-pending (5 min) cookies; sessions themselves are random database ids and are not affected. Rotate anytime — at worst, a user mid-step-up re-verifies.
  • Confirm the affected feature works end to end.
  • Keep the previous key/secret available (offline) until verification passes, then destroy it.
  • Record the rotation (date, which secret) in your change log for the audit trail.