When building cloud applications that handle sensitive data, encryption isn’t optional, it’s essential. But there’s an important difference between just doing the basic encryption vs. implementing it correctly at scale. In this article, we’ll explore envelope encryption, a pattern that AWS, Google Cloud, and Azure all use internally and recommend for production applications.
What Is Envelope Encryption?
Envelope encryption is a cryptographic pattern where you use two layers of keys:
- Data Encryption Key (DEK): A symmetric key that encrypts your actual data locally
- Key Encryption Key (KEK): A KMS-managed master key that encrypts the DEK
Think of it like a safety deposit box inside another safety deposit box. Your jewelry (I mean ‘data’) are locked in the inner box (encrypted with DEK), and the key to that inner box is itself locked in the outer box (DEK encrypted with KEK stored in cloud KMS).
Why do we need this approach? Let’s discuss it.
The Problem with Direct KMS Encryption
In traditional approach, you just use KMS as it is. It is slow & expensive. That becomes obvious when you try to encrypt large data.
Large Data → Send to KMS → Encrypted Data (slow, expensive)
But, that is not the only issue, we can list the issues as below:
- Performance: Sending 10GB files to KMS is painfully slow
- Cost: Every encryption operation costs money
- Limits: Most KMS services have payload size limits (4KB for AWS KMS)
- Availability: Requires network connectivity for every operation
The Envelope Encryption Solution
The envelope encryption solution solves a bunch of issues we just described. The following schema explains how it works.
First, your data gets encrypted locally using a fast symmetric key (DEK), then only that tiny key gets sent to KMS for encryption. You end up storing both pieces together (the encrypted data and the encrypted key) so decryption just reverses the process.
Data → DEK (local) → Encrypted Data (fast)
DEK → KMS Key → Encrypted DEK (small, cheap)
Store: [Encrypted Data + Encrypted DEK] together
Why This Is Superior
1. Massive Performance Gains
Problem: KMS network calls are slow Solution: Only the tiny DEK (32 bytes) goes to KMS
2. Dramatic Cost Reduction
Real pricing example (GCP KMS at $0.03 per 10,000 operations):
- Direct encryption: 10,000 files = $0.03
- Envelope encryption: 10,000 files = 1 KMS call = $0.000003
That’s a 10,000x cost reduction for bulk operations.
Production Implementation with Google Cloud
Prerequisites: Set Up Infrastructure
# Enable required APIs
gcloud services enable cloudkms.googleapis.com secretmanager.googleapis.com
# Create KMS resources
gcloud kms keyrings create production-keyring --location=global
gcloud kms keys create envelope-key \
--location=global \
--keyring=production-keyring \
--purpose=encryption
# Create service account
gcloud iam service-accounts create envelope-encryption-sa \
--display-name="Envelope Encryption Service Account"
# Grant minimal KMS permissions (specific key only)
gcloud kms keys add-iam-policy-binding envelope-key \
--location=global \
--keyring=production-keyring \
--member="serviceAccount:envelope-encryption-sa@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/cloudkms.cryptoKeyEncrypterDecrypter"
# Grant Secret Manager access (if needed)
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:envelope-encryption-sa@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
Production-Ready Implementation
import os
import json
import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from google.cloud import kms
from google.cloud import secretmanager
from google.api_core.exceptions import AlreadyExists
import logging
logger = logging.getLogger(__name__)
class EnvelopeEncryption:
def __init__(self, project_id, location, keyring, key_name):
self.project_id = project_id
self.kms_client = kms.KeyManagementServiceClient()
self.secret_client = secretmanager.SecretManagerServiceClient()
self.key_name = f"projects/{project_id}/locations/{location}/keyRings/{keyring}/cryptoKeys/{key_name}"
def encrypt_data(self, plaintext_data: str, metadata: dict = None) -> dict:
"""Encrypt data using envelope encryption pattern"""
# Generate fresh DEK (AES-256)
dek = AESGCM.generate_key(bit_length=256)
# Encrypt data locally with DEK
aesgcm = AESGCM(dek)
nonce = os.urandom(12) # 96-bit nonce for AES-GCM
encrypted_data = aesgcm.encrypt(nonce, plaintext_data.encode(), None)
# Encrypt DEK with KMS
encrypt_response = self.kms_client.encrypt(
request={
"name": self.key_name,
"plaintext": dek
}
)
# Create envelope
envelope = {
"encrypted_data": base64.b64encode(nonce + encrypted_data).decode(),
"encrypted_dek": base64.b64encode(encrypt_response.ciphertext).decode(),
"metadata": metadata or {}
}
return envelope
def decrypt_data(self, envelope: dict) -> str:
"""Decrypt data using envelope encryption pattern"""
try:
# Decrypt DEK using KMS (automatically handles key versions)
encrypted_dek = base64.b64decode(envelope["encrypted_dek"])
decrypt_response = self.kms_client.decrypt(
request={
"name": self.key_name,
"ciphertext": encrypted_dek
}
)
dek = decrypt_response.plaintext
# Decrypt data locally
encrypted_blob = base64.b64decode(envelope["encrypted_data"])
nonce = encrypted_blob[:12] # First 12 bytes
ciphertext = encrypted_blob[12:] # Rest is encrypted data
aesgcm = AESGCM(dek)
plaintext_data = aesgcm.decrypt(nonce, ciphertext, None)
return plaintext_data.decode()
except Exception as e:
logger.error(f"Decryption failed: {type(e).__name__}")
raise RuntimeError("Failed to decrypt data")
def store_encrypted_secret(self, secret_data: str, secret_id: str) -> str:
"""Store encrypted data in Secret Manager (max ~60KB)"""
envelope = self.encrypt_data(secret_data)
secret_payload = json.dumps(envelope)
# Check size limit (Secret Manager: ~64KB)
if len(secret_payload.encode()) > 60000:
raise ValueError("Data too large for Secret Manager. Use Cloud Storage for large files.")
secret_name = f"envelope-{secret_id}"
parent = f"projects/{self.project_id}"
# Create secret (idempotent)
try:
self.secret_client.create_secret(
request={
"parent": parent,
"secret_id": secret_name,
"secret": {"replication": {"automatic": {}}}
}
)
except AlreadyExists:
pass # Secret already exists
# Add version
self.secret_client.add_secret_version(
request={
"parent": f"{parent}/secrets/{secret_name}",
"payload": {"data": secret_payload.encode()}
}
)
return secret_name
# Usage example
if __name__ == "__main__":
encryptor = EnvelopeEncryption(
project_id="your-project",
location="global",
keyring="production-keyring",
key_name="envelope-key"
)
# Encrypt sensitive data
sensitive_data = "database_password=super_secret_123"
envelope = encryptor.encrypt_data(sensitive_data)
# Decrypt it back
decrypted = encryptor.decrypt_data(envelope)
print(f"Decrypted: {decrypted}")
Best Practices
1. DEK Management
- Generate fresh DEKs for each encryption operation
- Never log or persist decrypted DEKs
- Keep DEKs in memory only during processing
2. Error Handling
from google.api_core.exceptions import GoogleAPICallError
from cryptography.exceptions import InvalidTag
def safe_decrypt(self, envelope):
try:
return self.decrypt_data(envelope)
except (GoogleAPICallError, InvalidTag) as e:
logger.error(f"Decryption failed: {type(e).__name__}")
raise RuntimeError("Decryption failed")
3. Monitoring (Google Cloud)
Set up alerts for:
- KMS operation failures:
resource.type="cloudkms_crypto_key" AND severity>=ERROR
- Unusual access patterns:
protoPayload.methodName="google.cloud.kms.v1.KeyManagementService.Decrypt"
4. Storage Strategy
- Small secrets (< 60KB): Store envelope in Secret Manager
- NOTE: Python code sample I shared above stores in secret manager
- Large files: Store encrypted data in Cloud Storage, metadata in database
- Database records: Store envelope as JSON column
Common Pitfalls to Avoid
- Reusing DEKs across different data sets
- Storing DEKs in plaintext anywhere (logs, databases, files)
- Not handling key rotation gracefully in your application
- Ignoring size limits (Secret Manager: ~64KB, KMS: varies by provider)
- Catching broad exceptions that hide real security issues
Conclusion
I’ve seen too many developers skip proper encryption because it’s “too slow”, “too expensive” or they just don’t know (just between us, the biggest reason is the last one ;)). Envelope encryption changes that game completely. With 10,000x cost reduction and sub-millisecond encryption times, you’ll finally have the confidence to encrypt everything, and your future self will thank you when that security audit comes around.