Skip to main content

Key rotation runbook

This runbook describes how to rotate ENCRYPTION_KEY (inbox credentials and OAuth tokens) and AUTH_SECRET (session signing). See PRODUCT-AND-ARCHITECTURE.md §7.1 and §9.2/§9.3.


ENCRYPTION_KEY rotation

ENCRYPTION_KEY is used to encrypt inboxes.credentials_encrypted (IMAP/SMTP credentials and Gmail/Microsoft OAuth tokens). Key format: base64-encoded 32-byte key. Generate a new key with:

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Prerequisites

  • Database access (e.g. Neon SQL or app with both keys in env).
  • Ability to deploy with a new env var (e.g. Fly.io secrets).
  • Do not deploy the app with only the new key until all stored blobs have been re-encrypted.

Steps

  1. Generate a new key (see above). Store it securely; you will set it as ENCRYPTION_KEY after re-encryption.

  2. Run a one-off re-encryption script (or migration) that:

    • Reads every row in inboxes where credentials_encrypted IS NOT NULL.
    • For each row: decrypt with the old ENCRYPTION_KEY, then encrypt with the new key using the same algorithm (AES-256-GCM, see packages/email credential-encryption).
    • Writes the new ciphertext back to credentials_encrypted.
    • The script must have both old and new keys in env (e.g. ENCRYPTION_KEY = old, ENCRYPTION_KEY_NEW = new); never commit keys.
  3. Deploy the application with the new ENCRYPTION_KEY (replace the old value in Fly.io secrets or your env store). Remove the old key from the re-encryption script/env.

  4. Retire the old key — remove it from any secret stores and the re-encryption script. Do not use it again.

Rollback

If re-encryption fails partway: fix the script and re-run. Rows already re-encrypted with the new key will be decrypted with the old key (fail) then re-encrypted with the new key again (idempotent if you skip already-new-key rows, or run from a DB backup). If you have deployed the app with the new key before re-encryption completed, existing encrypted rows will fail to decrypt; restore from DB backup and re-run re-encryption, then deploy again.


AUTH_SECRET rotation

AUTH_SECRET is used by Lucia for session signing (cookie verification). Format: min 32 characters; generate with e.g. openssl rand -hex 32.

Effect of rotation

Rotating AUTH_SECRET invalidates all existing sessions. Every user will need to sign in again.

Steps

  1. Notify users (optional but recommended) that they will be logged out once.

  2. Deploy with the new AUTH_SECRET (e.g. update Fly.io secrets and deploy). No database migration is required — sessions remain in the DB but will no longer validate.

  3. Optional: Clear the sessions table (or leave entries to expire) to avoid stale rows. Lucia will reject old cookies regardless.

  4. Retire the old secret from your secret store.

Rollback

Revert the deployment to the previous release that had the old AUTH_SECRET. Users who signed in after the rotation will be logged out when you revert.