Encryption Key Rotation
Align encrypts OAuth credentials at rest with AES-256-GCM using a master key you provide. This page documents the procedure for rotating that key with zero downtime.
What the master key protects
ALIGN_MASTER_ENCRYPTION_KEY is used in two encryption domains:
- OAuth tokens - the
oauth_tokens.encrypted_valuecolumn (every connector's stored OAuth access/refresh tokens) is encrypted with AES-256-GCM by the gateway, in a versioned envelope ([version][iv][authTag][ciphertext]). This domain supports graceful multi-key rotation (below). - Tenant LLM API keys - the
tenant_llm_config.*_api_key_encryptedcolumns are encrypted with PostgreSQLpgcrypto(pgp_sym_encrypt), and the sameALIGN_MASTER_ENCRYPTION_KEYvalue is supplied as theapp.encryption_keysession setting (gatewaytenantConfigRoutes.ts, braindb.py).
The OAuth-token path can try several keys on decrypt, so it rotates with zero
downtime. The pgcrypto LLM-key columns cannot - pgp_sym_decrypt needs the
exact key that encrypted the value. Rotating ALIGN_MASTER_ENCRYPTION_KEY while
tenants have stored LLM keys therefore requires re-encrypting those columns in
the same change (see Rotating the LLM-key columns).
If no tenant has stored an LLM key, this section is moot.
Keys and environment variables
| Variable | Role |
|---|---|
ALIGN_MASTER_ENCRYPTION_KEY | Primary key. All new tokens are encrypted with this. 32 bytes, base64 or hex. |
ALIGN_PREVIOUS_ENCRYPTION_KEYS | Optional, comma-separated list of retired keys retained for decryption only during a rotation. |
Generate a new key:
openssl rand -base64 32
On decryption, Align tries the primary key first, then each retired key in turn, so tokens written with an old key keep working while you migrate. A malformed key in either variable fails loudly at startup rather than on the first token read.
Zero-downtime rotation procedure
┌─ new primary ─┐ ┌─ re-encrypt ─┐ ┌─ drop old ─┐
before │ MASTER = old │ ───► │ MASTER = new │ ───► │ MASTER = new │
MASTER=old│ PREVIOUS = - │ │ PREVIOUS=old │ │ PREVIOUS = - │
└───────────────┘ └──────────────┘ └────────────┘
1. Promote a new primary key
Set the new key as primary and keep the old key for decryption, then redeploy:
ALIGN_MASTER_ENCRYPTION_KEY=<new-key>
ALIGN_PREVIOUS_ENCRYPTION_KEYS=<old-key>
After this deploy, all newly written tokens use the new key; existing tokens still decrypt via the previous key. The system is fully functional in this state - you can pause here indefinitely.
2. Re-encrypt existing tokens
This step needs Align's help. The re-encryption script is not in the gateway image, which holds
only the compiled server. Contact Align support with your release version. We run it with you, using both keys and
the table-owner DATABASE_URL (not the restricted align_app role, which row-level security
limits to one tenant).
The script is safe to re-run: tokens already on the new key are skipped. It reports scanned,
migrated, already-current and failed. A non-zero failed count means some token could not
be decrypted with any configured key, usually because the old key left
ALIGN_PREVIOUS_ENCRYPTION_KEYS too early. Restore it and re-run before step 3.
3. Retire the old key
Once step 2 reports failed=0 and no tokens remain on the old key, remove it:
ALIGN_MASTER_ENCRYPTION_KEY=<new-key>
# ALIGN_PREVIOUS_ENCRYPTION_KEYS unset
Redeploy. The old key is now fully retired and can be destroyed in your secret store.
Kubernetes / Helm
The keys are delivered as Kubernetes secrets (see Secrets Management).
During a rotation, add ALIGN_PREVIOUS_ENCRYPTION_KEYS to the gateway secret
alongside the new ALIGN_MASTER_ENCRYPTION_KEY. Once step 2 is done, remove the
previous-keys entry and re-seal.
Rotating the LLM-key columns (pgcrypto)
Only needed if tenants have stored their own LLM API keys
(tenant_llm_config.*_api_key_encrypted). Because pgcrypto has no key
fallback, re-encrypt these columns in a single statement that decrypts with the
old key and re-encrypts with the new key, then deploy the new key. Run
this during the rotation window (step 1 above), as the DB admin/owner role:
-- Re-encrypt every tenant LLM key from <old> to <new>, in one transaction.
UPDATE tenant_llm_config SET
openai_api_key_encrypted = CASE WHEN openai_api_key_encrypted IS NOT NULL
THEN pgp_sym_encrypt(pgp_sym_decrypt(openai_api_key_encrypted, '<old-key>'), '<new-key>') END,
anthropic_api_key_encrypted = CASE WHEN anthropic_api_key_encrypted IS NOT NULL
THEN pgp_sym_encrypt(pgp_sym_decrypt(anthropic_api_key_encrypted, '<old-key>'), '<new-key>') END,
custom_api_key_encrypted = CASE WHEN custom_api_key_encrypted IS NOT NULL
THEN pgp_sym_encrypt(pgp_sym_decrypt(custom_api_key_encrypted, '<old-key>'), '<new-key>') END;
Sequence to avoid a decryption gap: run this UPDATE first, then immediately
deploy the new ALIGN_MASTER_ENCRYPTION_KEY so the app's app.encryption_key
matches the re-encrypted data. Until that deploy lands, the gateway/brain still
present the old key, so do the two steps back-to-back (or in a short maintenance
window). The OAuth-token re-encryption (step 2) can run independently on
its own schedule.
Recommended cadence
- Rotate at least annually, and immediately on any suspected key compromise or operator offboarding.
- Store keys in a managed secret store (AWS Secrets Manager, Vault). A future
KeyProvidercan source the key from KMS/Vault directly; the interface is already in place (services/gateway/src/auth/tokenEncryption.ts). - Per-tenant data keys are a possible future enhancement and are not required for this rotation flow.