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
-
Generate a new key (see above). Store it securely; you will set it as
ENCRYPTION_KEYafter re-encryption. -
Run a one-off re-encryption script (or migration) that:
- Reads every row in
inboxeswherecredentials_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, seepackages/emailcredential-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.
- Reads every row in
-
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. -
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
-
Notify users (optional but recommended) that they will be logged out once.
-
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. -
Optional: Clear the
sessionstable (or leave entries to expire) to avoid stale rows. Lucia will reject old cookies regardless. -
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.