Implementing Tenant-Managed Encryption Keys (BYOK) in Enterprise Applications
A technical guide to implementing Bring Your Own Key (BYOK) and envelope encryption using AWS KMS, Google Cloud KMS, and cryptographic shredding.
When selling cloud platform to banks, healthcare networks, or regulated financial institutions, standard "encryption at rest" managed by your cloud provider (e.g. standard AWS RDS default encryption) is rarely sufficient. Enterprise security teams want sovereign control over their sensitive data. They ask a decisive question:
"If our contract ends, or if a government subpoena is served to your company, how can we guarantee that you cannot read our data?"
The enterprise answer is Bring Your Own Key (BYOK), also known as Customer-Managed Encryption Keys (CMEK). With BYOK, the root cryptographic master key is created and maintained inside the customer's own cloud account (such as AWS KMS, Google Cloud KMS, or Azure Key Vault). The customer grants your cloud application role temporary permission to use the key.
If the customer ever decides to terminate their contract or suspects a security breach, they disable the key in their own cloud console. In less than two seconds, every record belonging to that customer across your databases, backups, and caches becomes completely indecipherable—a mechanism known as Cryptographic Shredding.
This guide covers the technical architecture of building BYOK using Envelope Encryption (NIST SP 800-57) without incurring prohibitive network latency or cloud API fees.
The Performance Dilemma: Why Direct KMS Encryption Fails
A naive approach to BYOK is calling AWS KMS directly whenever your application writes or reads a database record:
// NAIVE ANTI-PATTERN: Calling KMS for every record
ciphertext, _ := kmsClient.Encrypt(ctx, &kms.EncryptInput{
KeyId: customerKeyARN,
Plaintext: documentData,
})
In a production cloud platform, this naive approach collapses for three reasons:
- Intolerable Latency: Calling KMS over the public cloud network adds 15ms to 40ms of latency per query. An API endpoint loading 50 records would take two full seconds to respond.
- KMS Rate Limits: AWS KMS enforces a regional quota (typically 10,000 to 50,000 requests per second). A busy multi-tenant application will trigger
ThrottlingExceptionerrors during normal business hours. - Runaway Cloud Costs: AWS KMS charges $0.03 per 10,000 requests. Encrypting every database row directly can generate tens of thousands of dollars in monthly cloud API expenses.
The Solution: Envelope Encryption Architecture
To achieve zero-latency performance while preserving customer key sovereignty, follow the NIST SP 800-57 Envelope Encryption standard:
[ Customer AWS Account ] [ Your cloud platform Production Cluster ]
| |
Customer KEK |
(Key Encryption Key) |
| |
+--- 1. Call kms:GenerateDataKey ---------------> |
| v
|<-- 2. Return { Plaintext DEK, Encrypted DEK } --+
|
| 3. Encrypt data locally using
| AES-256-GCM in volatile RAM
v
[ Database Record ]
• encrypted_dek (stored on disk)
• ciphertext (stored on disk)
• iv_nonce (stored on disk)
- Key Encryption Key (KEK): The customer's master 256-bit symmetric key residing permanently inside their AWS KMS HSM. Plaintext KEK never leaves AWS hardware.
- Data Encryption Key (DEK): A unique, ephemeral 256-bit AES symmetric key generated by KMS.
- The Local Operation: Your backend application encrypts the document locally in RAM using high-speed AES-GCM (which takes less than 2 microseconds using hardware AES-NI CPU instructions).
- Persistent Storage: You store the Ciphertext alongside the Encrypted DEK. The plaintext DEK is securely discarded from memory.
Reading Data Back
When your application needs to read the record:
- Fetch the
encrypted_dekfrom the database. - Call
kms:Decryptagainst the customer's KMS Key ARN to unwrap theencrypted_dekinto a transient plaintext DEK. - Decrypt the ciphertext locally in RAM.
If the customer has disabled their KMS key in AWS, step 2 fails immediately with KMS.DisabledException. Your application cannot decrypt the record, enforcing the customer's kill switch instantly.
Implementation: Local AES-256-GCM Envelope Engine
Here is the production cryptographic envelope engine using standard library crypto/aes and crypto/cipher:
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
)
type EncryptedPayload struct {
EncryptedDEK []byte `json:"encrypted_dek"`
Ciphertext []byte `json:"ciphertext"`
Nonce []byte `json:"nonce"`
}
// EncryptLocal encrypts plaintext using local transient DEK with AES-GCM
func EncryptLocal(plaintext []byte, plaintextDEK []byte, encryptedDEK []byte, tenantID string) (*EncryptedPayload, error) {
block, err := aes.NewCipher(plaintextDEK)
if err != nil {
return nil, fmt.Errorf("creating aes cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("creating gcm: %w", err)
}
// 12-byte cryptographically secure random nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, fmt.Errorf("generating nonce: %w", err)
}
// Authenticated Data (AAD): Bind tenantID to prevent cross-tenant ciphertext swapping
additionalData := []byte(tenantID)
ciphertext := gcm.Seal(nil, nonce, plaintext, additionalData)
return &EncryptedPayload{
EncryptedDEK: encryptedDEK,
Ciphertext: ciphertext,
Nonce: nonce,
}, nil
}
// DecryptLocal decrypts ciphertext in RAM using unwrapped transient DEK
func DecryptLocal(payload *EncryptedPayload, plaintextDEK []byte, tenantID string) ([]byte, error) {
block, err := aes.NewCipher(plaintextDEK)
if err != nil {
return nil, fmt.Errorf("creating aes cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("creating gcm: %w", err)
}
additionalData := []byte(tenantID)
plaintext, err := gcm.Open(nil, payload.Nonce, payload.Ciphertext, additionalData)
if err != nil {
return nil, fmt.Errorf("decryption failed (tampering or bad key): %w", err)
}
return plaintext, nil
}
Ephemeral DEK Caching with Strict TTL
To avoid calling kms:Decrypt for every single query while preserving the customer's ability to revoke access quickly, implement a strictly bound in-memory DEK cache:
type DEKCache struct {
cache *ristretto.Cache
}
func (c *DEKCache) GetOrDecrypt(ctx context.Context, kmsClient KMSClient, encryptedDEK []byte, keyARN string) ([]byte, error) {
cacheKey := sha256.Sum256(encryptedDEK)
if val, found := c.cache.Get(cacheKey); found {
return val.([]byte), nil
}
// Cache miss: Call AWS KMS Decrypt
plaintextDEK, err := kmsClient.Decrypt(ctx, encryptedDEK, keyARN)
if err != nil {
return nil, err
}
// CRITICAL SECURITY RULE: Maximum 5-minute TTL!
// If the customer revokes their KMS key, all access halts within 5 minutes.
c.cache.SetWithTTL(cacheKey, plaintextDEK, 1, 5*time.Minute)
return plaintextDEK, nil
}
By capping the in-memory cache TTL at 5 minutes, you reduce KMS network calls by over 99.8% while guaranteeing that any customer key revocation takes full effect across your entire application fleet in under 300 seconds.
Summary Checklist for Production
- Follow Envelope Encryption: Never encrypt entire payloads directly via cloud KMS APIs.
- Bind Additional Authenticated Data (AAD): Pass
tenant_idas GCM Associated Data to prevent an attacker from moving encrypted ciphertext between tenant records. - Enforce Short DEK TTLs: Cap in-memory plaintext DEK caches at a maximum of 5 minutes.
- Handle KMS Errors Cleanly: Gracefully catch
KMS.DisabledExceptionand display an informative administrative notification rather than throwing generic 500 errors. - Automate IAM Policy Generators: Provide enterprise customers with a pre-configured CloudFormation or Terraform snippet to establish their KMS Key Policy in minutes.
Frequently Asked Questions
Why can't a cloud application simply encrypt all data directly using the customer's KMS key?
Calling AWS KMS over the network for every single database row read or write adds 15 to 40 milliseconds of latency and incurs high KMS API request fees ($0.03 per 10,000 calls). Envelope encryption eliminates this by using local DEKs.
What permissions does the platform vendor require in the customer's AWS account?
The customer grants a scoped IAM Key Policy allowing the platform vendor's IAM role to execute only `kms:GenerateDataKey` and `kms:Decrypt` on that specific Key ARN, maintaining total customer ownership.
What happens if a customer accidentally disables their KMS key in AWS?
The cloud application receives a `KMS.DisabledException`. The platform gracefully enters a paused state for that tenant, alerting the customer administrator to re-enable their key within their AWS console.
What is the difference between BYOK and HYOK (Hold Your Own Key)?
In BYOK (Bring Your Own Key), the customer owns the root key in cloud KMS, but operations occur in the cloud environment. In HYOK (Hold Your Own Key), the plaintext data can never leave the customer's on-premises hardware security module (HSM).
How does BYOK protect against government subpoenas or cloud provider breaches?
Because the root encryption keys reside inside the customer's corporate cloud account, the platform vendor cannot decrypt customer data to comply with external third-party subpoenas without the customer's direct knowledge and authorization.
Can search indexes function when records are encrypted under tenant KMS keys?
Exact search can function using blind indexing (HMAC salt hashes), but full-text and range searches must be executed in memory or within an isolated, tenant-partitioned search cluster.