Skip to main content

Encryption Key Rotation

Align encrypts OAuth credentials at rest with AES-256-GCM using a master key you provide. This page documents the supported procedure for rotating that key with zero downtime - a named control for SOC 2 key lifecycle management.

What the master key protects

ALIGN_MASTER_ENCRYPTION_KEY is used in two encryption domains:

  1. OAuth tokens - the oauth_tokens.encrypted_value column (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).
  2. Tenant LLM API keys - the tenant_llm_config.*_api_key_encrypted columns are encrypted with PostgreSQL pgcrypto (pgp_sym_encrypt), and the same ALIGN_MASTER_ENCRYPTION_KEY value is supplied as the app.encryption_key session setting (gateway tenantConfigRoutes.ts, brain db.py).
pgcrypto has no key fallback

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

VariableRole
ALIGN_MASTER_ENCRYPTION_KEYPrimary key. All new tokens are encrypted with this. 32 bytes, base64 or hex.
ALIGN_PREVIOUS_ENCRYPTION_KEYSOptional, 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

Run the rotation job with both keys in the environment. It decrypts every stored token (with whichever key works) and re-writes it under the new primary key:

# Dry run first - reports what would change, writes nothing
DATABASE_URL=<admin-url> \
ALIGN_MASTER_ENCRYPTION_KEY=<new-key> \
ALIGN_PREVIOUS_ENCRYPTION_KEYS=<old-key> \
pnpm --filter @align/gateway rotate:keys --dry-run

# Real run
DATABASE_URL=<admin-url> \
ALIGN_MASTER_ENCRYPTION_KEY=<new-key> \
ALIGN_PREVIOUS_ENCRYPTION_KEYS=<old-key> \
pnpm --filter @align/gateway rotate:keys

Use the admin / migration DATABASE_URL (the table owner), not the restricted align_app role - the job must read every tenant's rows, which Row-Level Security would otherwise filter out. The job is idempotent: tokens already on the primary key are skipped, so it is safe to re-run.

The summary line reports scanned, migrated, already-current, and failed. A non-zero failed count means some token could not be decrypted with any configured key - usually the old key was removed from ALIGN_PREVIOUS_ENCRYPTION_KEYS too early. Restore it and re-run before step 3.

3. Retire the old key

Once the job 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, run the job (e.g. as a one-off kubectl exec into the gateway pod or a Job), then 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 (script above) can run independently on its own schedule.

  • 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 KeyProvider can 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.