encrypt the actual data; CMKs encrypt the DEKs.
2. Why Envelope Encryption?: Rotating a CMK never requires decrypting the underlying data. You only rotate the encrypted DEK wrapper. This reduces rotation scope from terabytes of payload to kilobytes of metadata.
3. Lazy vs. Eager Re-encryption: Eager re-encryption triggers immediate data migration on rotation, causing I/O contention. Lazy re-encryption defers re-encryption until the next read/write cycle. It trades minimal compute overhead during access for zero downtime during rotation.
4. Versioning: Every ciphertext must embed the key version used. This enables backward compatibility, rollbacks, and precise audit trails.
Step-by-Step Implementation
Step 1: Define Key Metadata Structure
Store version, algorithm, and CMK ARN alongside ciphertext.
interface EncryptedPayload {
version: number;
cmkArn: string;
encryptedDataKey: string; // Base64 or hex-encoded
encryptedData: string; // Base64 or hex-encoded
iv: string; // Initialization vector
authTag?: string; // For AEAD modes like AES-GCM
}
Step 2: Envelope Encryption Function
Generate a DEK, encrypt it with the CMK, then encrypt the payload.
import { createCipheriv, randomBytes } from 'crypto';
import { KMSClient, GenerateDataKeyCommand, EncryptCommand } from '@aws-sdk/client-kms';
const kms = new KMSClient({ region: 'us-east-1' });
async function envelopeEncrypt(
plaintext: Buffer,
cmkArn: string,
keyVersion: number
): Promise<EncryptedPayload> {
// 1. Generate DEK via KMS (returns plaintext + encrypted DEK)
const dataKeyResponse = await kms.send(
new GenerateDataKeyCommand({ KeyId: cmkArn, KeySpec: 'AES_256' })
);
const plaintextKey = dataKeyResponse.Plaintext!;
const encryptedDataKey = dataKeyResponse.CiphertextBlob!;
const iv = randomBytes(12); // AES-GCM standard IV
// 2. Encrypt payload with DEK
const cipher = createCipheriv('aes-256-gcm', plaintextKey, iv);
let encrypted = cipher.update(plaintext);
encrypted = Buffer.concat([encrypted, cipher.final()]);
const authTag = cipher.getAuthTag();
// 3. Return structured payload
return {
version: keyVersion,
cmkArn,
encryptedDataKey: encryptedDataKey.toString('base64'),
encryptedData: encrypted.toString('base64'),
iv: iv.toString('base64'),
authTag: authTag.toString('base64')
};
}
Step 3: Lazy Re-Encryption on Read
Decrypt with the stored version's CMK. If version < current, re-encrypt with the latest key.
import { createDecipheriv, randomBytes } from 'crypto';
import { DecryptCommand, GenerateDataKeyCommand } from '@aws-sdk/client-kms';
async function envelopeDecrypt(
payload: EncryptedPayload,
currentCmkArn: string,
currentVersion: number
): Promise<{ plaintext: Buffer; needsReencryption: boolean }> {
// 1. Decrypt DEK using the versioned CMK
const decryptResponse = await kms.send(
new DecryptCommand({ CiphertextBlob: Buffer.from(payload.encryptedDataKey, 'base64') })
);
const plaintextKey = decryptResponse.Plaintext!;
// 2. Decrypt payload
const iv = Buffer.from(payload.iv, 'base64');
const authTag = Buffer.from(payload.authTag!, 'base64');
const decipher = createDecipheriv('aes-256-gcm', plaintextKey, iv);
decipher.setAuthTag(authTag);
let plaintext = decipher.update(Buffer.from(payload.encryptedData, 'base64'));
plaintext = Buffer.concat([plaintext, decipher.final()]);
// 3. Determine if lazy re-encryption is needed
const needsReencryption = payload.version < currentVersion;
return { plaintext, needsReencryption };
}
Step 4: Rotation Trigger & Version Bump
Rotation is a control-plane operation. Update the current version, disable/alias the old CMK, and let lazy re-encryption handle data migration.
// Metadata store (Redis, DynamoDB, or config service)
const KEY_METADATA = {
currentVersion: 2,
currentCmkArn: 'arn:aws:kms:us-east-1:123456789012:key/b2f3a4c5-...',
rotationPolicy: '90d'
};
async function rotateKey() {
// 1. Create new CMK in KMS (or rely on auto-rotation policy)
// 2. Update metadata
KEY_METADATA.currentVersion += 1;
KEY_METADATA.currentCmkArn = 'arn:aws:kms:us-east-1:123456789012:key/new-key-id';
// 3. Schedule old key for deletion (AWS supports 7-30 day pending deletion)
// 4. Emit audit event
console.log(`Key rotated to v${KEY_METADATA.currentVersion}`);
}
Architecture Rationale Summary:
- Cloud KMS handles HSM-backed storage, FIPS validation, and audit logging natively.
- Envelope encryption reduces rotation blast radius to metadata only.
- Lazy re-encryption eliminates migration downtime while maintaining cryptographic freshness.
- Version embedding ensures deterministic decryption and safe rollbacks.
Pitfall Guide
-
Rotating DEKs Directly Instead of KEKs
Mistake: Teams generate new DEKs for every rotation and attempt to re-encrypt all data immediately.
Impact: I/O storms, service degradation, and potential data loss if migration fails mid-cycle.
Best Practice: Rotate the KEK/CMK. DEKs are ephemeral and scoped to data partitions. Let envelope encryption absorb the rotation.
-
Breaking Backward Compatibility During Rotation
Mistake: Overwriting ciphertext metadata or failing to store the key version used during encryption.
Impact: Historical data becomes undecryptable. Rollback is impossible.
Best Practice: Always embed version and cmkArn in the ciphertext payload. Maintain a key version registry that supports multiple active versions during transition windows.
-
Ignoring Key Version Metadata in Storage
Mistake: Storing encrypted data without version tags, forcing application-level version tracking that drifts from reality.
Impact: Decryption failures, silent data corruption, or fallback to insecure defaults.
Best Practice: Treat key metadata as part of the data contract. Validate version existence before decryption. Reject or quarantine records with missing/invalid versions.
-
Synchronous Bulk Re-encryption
Mistake: Triggering immediate re-encryption of entire tables or object stores on rotation.
Impact: Database locks, cache invalidation storms, SLA breaches, and cost spikes.
Best Practice: Implement lazy re-encryption. Re-encrypt only on next access. Use background workers for high-value cold data if compliance requires eager rotation.
-
Storing Rotated Keys in Plaintext or Insecure Locations
Mistake: Logging DEKs, caching plaintext keys in application memory, or storing rotated KEKs in configuration files.
Impact: Complete cryptographic bypass if application logs or config repos are compromised.
Best Practice: Never log or cache plaintext keys. Use KMS Decrypt calls on-demand. Enable KMS CloudTrail/audit logging. Rotate application-level secrets separately.
-
Missing Rotation Audit Trails
Mistake: Rotating keys without logging who, when, why, and which versions were affected.
Impact: Failed compliance audits, inability to correlate breaches with key lifecycle events.
Best Practice: Emit structured audit events on every rotation, decryption, and version mismatch. Integrate with SIEM. Enforce immutable audit storage.
-
Over-Rotation Causing Performance Degradation
Mistake: Rotating keys daily or hourly without workload profiling.
Impact: Excessive KMS API calls, increased latency, and unnecessary DEK generation overhead.
Best Practice: Align rotation frequency with data sensitivity and regulatory requirements. 90β365 days is standard for most workloads. Use KMS auto-rotation policies to eliminate manual scheduling.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup / MVP | KMS Auto-Rotation + Eager Re-encryption | Simplicity over scale; limited data volume | Low KMS costs; moderate compute during rotation |
| Regulated Enterprise | Envelope Encryption + Lazy Re-encryption | Compliance requires frequent rotation without downtime | Higher initial dev cost; lower operational risk |
| High-Throughput Data Pipeline | Envelope Encryption + Batch Lazy Re-encryption | Prevents I/O contention; aligns with partitioned data | Minimal KMS overhead; predictable compute scheduling |
| Legacy Migration | Hybrid: Versioned Ciphertext + Gradual Rollout | Avoids breaking existing systems; enables phased adoption | Moderate migration cost; reduces breach liability |
Configuration Template
# key-rotation-config.yaml
encryption:
mode: envelope
algorithm: AES-256-GCM
iv_bytes: 12
key_hierarchy:
cmk:
provider: aws_kms
auto_rotate: true
rotation_interval_days: 180
pending_deletion_days: 7
dek:
generation: kms_managed
scope: per_record
lifecycle:
reencryption_strategy: lazy
max_concurrent_reencryption_workers: 4
version_tracking:
enabled: true
storage: redis
ttl_days: 365
audit:
enabled: true
sink: cloudwatch_logs
include_decryption_events: true
include_version_mismatches: true
Quick Start Guide
- Create a CMK in your cloud provider: Use AWS KMS, GCP KMS, or Azure Key Vault. Enable auto-rotation (default 365 days).
- Deploy the envelope encryption module: Integrate the TypeScript functions above into your data access layer. Ensure
version and cmkArn are stored with every encrypted record.
- Configure lazy re-encryption: Add a version check to your read path. If
payload.version < currentVersion, call envelopeEncrypt with the new CMK and persist the updated payload.
- Verify rotation behavior: Manually trigger a version bump in your metadata store. Read an old record. Confirm it decrypts successfully and writes back with the new version.
- Enable audit logging: Route KMS
Decrypt, GenerateDataKey, and application rotation events to your SIEM. Validate that every rotation event is captured with version, timestamp, and caller identity.
You are now running cryptographic agility. Rotation is a control-plane operation, not a data migration event.