Encryption Key Rotation
Rotate the Forkpoint encryption master key used for at-rest secrets (Jira, GitHub, Slack tokens, webhook signing keys) without downtime.
Encryption Key Rotation
Forkpoint encrypts sensitive fields (Jira API tokens, GitHub personal access tokens, Slack OAuth tokens, webhook signing secrets) at rest with AES-256-GCM. The key is derived from ENCRYPTION_MASTER_KEY. This document covers how to rotate it without downtime.
Why segregate from JWT_SECRET?
Before this change ENCRYPTION_MASTER_KEY was reused from JWT_SECRET. That coupling meant a single leak would let an attacker both forge sessions AND decrypt every third-party credential in the database. Splitting the two keys means they can be rotated independently and have their own blast radius.
During migration, if ENCRYPTION_MASTER_KEY is not set the API falls back to JWT_SECRET and logs a boot warning. The fallback exists only to avoid breaking existing installs and will be removed in a future release — set a dedicated key as soon as possible.
Rotation procedure
1. Generate a new 32-byte key
openssl rand -base64 48
Any string >= 16 characters works; 32+ bytes of entropy is recommended.
2. Set the env var and deploy
Put the new value in ENCRYPTION_MASTER_KEY and deploy. The EncryptionService accepts an optional fallback key: while ENCRYPTION_MASTER_KEY differs from JWT_SECRET, the API uses the new key for encryption and keeps JWT_SECRET wired in as a decrypt fallback, so previously-written blobs keep working.
3. Rewrap existing blobs (optional)
Every time a user saves their Jira / GitHub / Slack config the token is re-encrypted with the new primary key. If you want to proactively rewrap all records, access each encrypted resource once — the API emits a structured log { event: "encryption-rewrap-needed" } whenever it fell back to the old key, so you can grep the logs to track progress.
For fully lazy rotation, do nothing — records rewrap themselves as users touch them.
4. Remove the fallback
Once your logs no longer show encryption-rewrap-needed entries, you can safely rotate JWT_SECRET independently without impacting encrypted data. The EncryptionService only emits the fallback when ENCRYPTION_MASTER_KEY !== JWT_SECRET, so nothing else needs to change in code.
What gets encrypted?
- Jira API tokens (
jira_configs.encryptedToken) - GitHub personal access tokens (
github_configs.encryptedToken) - Slack bot / user tokens (
slack_configs.encryptedToken) - Webhook signing secrets (
webhooks.encryptedSecret)
JWTs and OAuth session cookies are signed, not encrypted, by JWT_SECRET — they are unaffected by this rotation procedure.