Encryption at Rest: TDE, Key Management, and Performance
Learn Transparent Data Encryption (TDE), application-level encryption, and key management using AWS KMS and HashiCorp Vault. Performance overhead explained.
Encryption at Rest: TDE, Key Management, and Performance
Data breaches make headlines, but the real question is what happens when someone walks away with your database files. Encryption at rest is the answer: without the keys, the data is just noise.
This guide covers Transparent Data Encryption at the database level, application-layer encryption for sensitive fields, key management infrastructure, and the performance realities you need to understand before deploying to production.
flowchart LR
subgraph KEK["Key Hierarchy"]
MK[("Master Key<br/>KMS/HSM")]
end
subgraph DEK["Data Key Layer"]
DK1[("DEK v1<br/>User Data")]
DK2[("DEK v2<br/>User Data")]
DK3[("DEK<br/>Config Data")]
end
subgraph Storage["Storage Layer"]
E1[("Encrypted<br/>User Table")]
E2[("Encrypted<br/>Config Table")]
E3[("Encrypted<br/>Backup")]
end
MK -->|encrypts| DK1
MK -->|encrypts| DK2
MK -->|encrypts| DK3
DK1 -->|encrypts| E1
DK2 -->|encrypts| E2
DK3 -->|encrypts| E3
E1 -.->|rotate| DK2
Understanding Encryption at Rest
Encryption at rest protects stored data by encrypting it when written to disk. The data remains encrypted while stored, and only becomes plaintext when read by authorized processes with access to the decryption keys.
The fundamental components:
- Plaintext: Original, readable data
- Ciphertext: Encrypted data that appears random without the key
- Encryption algorithm: Mathematical transformation (AES-256, ChaCha20)
- Key: Secret value used for encryption/decryption
- Key encryption key (KEK): Master key that encrypts data encryption keys
- Data encryption key (DEK): Key that actually encrypts the data
This hierarchy—DEKs protected by KEKs, KEKs stored in key management systems—allows rotation, revocation, and access control without re-encrypting all data.
Transparent Data Encryption (TDE)
TDE encrypts data at the storage level, between the database and disk. The database engine handles encryption and decryption transparently—applications continue working without modification.
PostgreSQL TDE with pgcrypto
PostgreSQL doesn’t have native TDE like Oracle or SQL Server, but you can achieve similar results:
-- Enable pgcrypto extension
CREATE EXTENSION pgcrypto;
-- Encrypt specific columns
CREATE TABLE customer_data (
id SERIAL PRIMARY KEY,
name TEXT,
ssn_encrypted BYTEA ENCRYPT WITH ('AES256'),
credit_card_encrypted BYTEA ENCRYPT WITH ('AES256')
);
-- Insert encrypted data
INSERT INTO customer_data (name, ssn_encrypted, credit_card_encrypted)
VALUES (
'Jane Smith',
pgp_sym_encrypt('123-45-6789', 'encryption_key_here'),
pgp_sym_encrypt('4111111111111111', 'encryption_key_here')
);
MySQL TDE
MySQL Enterprise and MySQL 8.0+ support TDE:
-- Enable TDE for MySQL
ALTER INSTANCE ENABLE TDE_KEYRING;
-- Create encrypted tablespace
CREATE TABLE sensitive_data (
id INT PRIMARY KEY,
data VARCHAR(255)
) ENCRYPTION='Y';
Microsoft SQL Server TDE
SQL Server TDE encrypts entire databases:
-- Create master key
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'ComplexPassword123!';
-- Create certificate
CREATE CERTIFICATE MyServerCert WITH SUBJECT = 'TDE Certificate';
-- Create database encryption key
USE mydatabase;
CREATE DATABASE ENCRYPTION KEY
WITH ALGORITHM = AES_256
ENCRYPTION BY SERVER CERTIFICATE MyServerCert;
-- Enable encryption
ALTER DATABASE mydatabase SET ENCRYPTION ON;
TDE Limitations
TDE has fundamental limitations:
- Key per database: TDE typically encrypts at the database or tablespace level, not the row level
- Memory exposure: Data is plaintext in buffer pool after decryption
- Access control still applies: Anyone with database access sees decrypted data
- Backup encryption separate: Database backups may not inherit TDE protection
Application-Level Encryption
For sensitive fields—PII, credentials, financial data—application-layer encryption provides finer-grained control. You encrypt specific columns before sending data to the database.
Column-Level Encryption Example
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
class FieldEncryptor:
def __init__(self, master_key: bytes):
# Derive key from master key
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=b'your-app-salt', # In production, use unique salt per field
iterations=480000,
)
key = base64.urlsafe_b64encode(kdf.derive(master_key))
self.fernet = Fernet(key)
def encrypt(self, plaintext: str) -> str:
"""Returns base64-encoded ciphertext"""
return self.fernet.encrypt(plaintext.encode()).decode()
def decrypt(self, ciphertext: str) -> str:
"""Decrypts base64-encoded ciphertext"""
return self.fernet.decrypt(ciphertext.encode()).decode()
# Usage
encryptor = FieldEncryptor(b'super-secret-master-key')
# Encrypt before storing
email = 'user@example.com'
encrypted_email = encryptor.encrypt(email)
# Store encrypted_email in database
# Decrypt when reading
decrypted_email = encryptor.decrypt(encrypted_email)
Searchable Encryption
A challenge with application-layer encryption: how do you search encrypted data? Options include:
-
Deterministic encryption: Same plaintext always produces same ciphertext, enabling equality searches but leaking pattern information
-
Searchable encryption schemes: Specialized algorithms that allow searches without full decryption (research actively evolving)
-
Search index separate from data: Maintain an unencrypted index mapping search terms to encrypted record IDs (requires careful access control)
-- Pattern-matching on encrypted data is impossible
-- You need application-level search
CREATE INDEX idx_email_hash ON users(email_hash);
Encryption in Transit vs. At Rest
Don’t confuse the two:
| Protection | What it guards | Implementation |
|---|---|---|
| Encryption in transit | Data moving over network | TLS/SSL, mTLS |
| Encryption at rest | Data stored on disk | TDE, application-layer |
| Encryption in use | Data in memory | Confidential computing (emerging) |
You typically need both. TDE protects against physical media theft. TLS protects against network interception. Application-layer encryption provides defense-in-depth if other layers fail.
Key Management
Here’s the uncomfortable truth about encryption: it’s only as strong as your key management. Hardcoded keys, unrotated secrets, inadequate access controls—any of these undermine the entire encryption strategy.
AWS KMS Integration
AWS Key Management Service provides centralized key storage and management:
import boto3
import base64
from cryptography.fernet import Fernet
def encrypt_with_kms(aws_region: str, key_id: str, plaintext: str) -> dict:
kms = boto3.client('kms', region_name=aws_region)
# Generate data key from KMS
response = kms.generate_data_key(
KeyId=key_id,
KeySpec='AES_256'
)
# Encrypt plaintext with data key
data_key = response['Plaintext']
encrypted_data_key = response['CiphertextBlob']
fernet = Fernet(data_key)
encrypted_plaintext = fernet.encrypt(plaintext.encode())
return {
'encrypted_data_key': base64.b64encode(encrypted_data_key).decode(),
'ciphertext': base64.b64encode(encrypted_plaintext).decode()
}
def decrypt_with_kms(aws_region: str, encrypted_blob: dict) -> str:
kms = boto3.client('kms', region_name=aws_region)
# Decrypt the data key
encrypted_data_key = base64.b64decode(encrypted_blob['encrypted_data_key'])
response = kms.decrypt(CiphertextBlob=encrypted_data_key)
data_key = response['Plaintext']
# Decrypt the data
ciphertext = base64.b64decode(encrypted_blob['ciphertext'])
fernet = Fernet(data_key)
return fernet.decrypt(ciphertext).decode()
KMS best practices:
- Use CMKs (Customer Master Keys) for production, not AWS-managed keys
- Implement key rotation (automatic or manual)
- Use key policies and IAM for access control
- Enable and audit CloudTrail logging for key usage
HashiCorp Vault for Key Management
Vault provides a comprehensive secrets management solution:
# Enable transit secrets engine for encryption as a service
vault secrets enable transit
# Create an encryption key
vault write -f transit/keys/myapp-key
# Encrypt data via Vault API
vault write transit/encrypt/myapp-key \
plaintext=$(echo -n "sensitive data" | base64)
# Decrypt via Vault API
vault write transit/decrypt/myapp-key \
ciphertext="vault:v1:..."
# Rotate the key (new version available, old preserved)
vault write -f transit/keys/myapp-key/rotate
import hvac
def encrypt_with_vault(vault_url: str, token: str, key: str, plaintext: str) -> str:
client = hvac.Client(url=vault_url, token=token)
response = client.secrets.transit.encrypt_data(
name=key,
plaintext=plaintext
)
return response['data']['ciphertext']
def decrypt_with_vault(vault_url: str, token: str, key: str, ciphertext: str) -> str:
client = hvac.Client(url=vault_url, token=token)
response = client.secrets.transit.decrypt_data(
name=key,
ciphertext=ciphertext
)
return response['data']['plaintext']
Vault advantages over KMS:
- Single control plane for all secrets (keys, passwords, certificates)
- Fine-grained access policies
- Audit logging of all secret access
- Dynamic secrets (on-demand credentials)
- Encryption as a service (no key material leaves Vault)
Key Rotation Strategy
Key rotation limits the blast radius of a compromised key:
import os
from functools import wraps
from cryptography.fernet import Fernet
class KeyRotator:
def __init__(self, kms_client, key_id: str):
self.kms = kms_client
self.key_id = key_id
self._key_cache = {}
def get_dek(self, key_version: int) -> bytes:
"""Get data encryption key for specific version"""
if key_version in self._key_cache:
return self._key_cache[key_version]
# In production, retrieve from secure key storage
# This is simplified - use proper key versioning
response = self.kms.generate_data_key(
KeyId=self.key_id,
KeySpec='AES_256'
)
self._key_cache[key_version] = response['Plaintext']
return self._key_cache[key_version]
def rotate_and_reencrypt(self, old_version: int, new_version: int, records: list) -> list:
"""Re-encrypt records with new key version"""
old_key = self.get_dek(old_version)
new_key = self.get_dek(new_version)
result = []
for record in records:
# Decrypt with old key
fer_old = Fernet(old_key)
plaintext = fer_old.decrypt(record['encrypted_data'])
# Encrypt with new key
fer_new = Fernet(new_key)
result.append({
**record,
'encrypted_data': fer_new.encrypt(plaintext),
'key_version': new_version
})
return result
Performance Overhead
Encryption has measurable costs. Understanding them helps you design appropriately.
Benchmark: TDE Performance Impact
Typical overhead from TDE:
| Workload | No TDE | TDE Enabled | Overhead |
|---|---|---|---|
| Read-heavy (100% reads) | baseline | +2-5% | Minimal |
| Mixed (70/30 read/write) | baseline | +5-15% | Moderate |
| Write-heavy (100% writes) | baseline | +15-30% | Significant |
| Bulk load | baseline | +20-40% | Consider |
Mitigating Performance Impact
-
Hardware acceleration: AES-NI CPU instructions reduce overhead significantly
-
Key caching: Minimize key material access during operations
-
Encrypt only what matters: Column-level encryption for sensitive data, not entire database
-
Batch operations: Group multiple operations to amortize key access overhead
# Poor: Key access per row
for record in records:
encrypted = encrypt(record['sensitive_data']) # Key access each time
# Better: Batch encryption
data_key = get_data_key() # One key access
fernet = Fernet(data_key)
for record in records:
encrypted = fernet.encrypt(record['sensitive_data'].encode())
- Asynchronous encryption for non-critical paths: Decouple encryption from write path when eventual consistency is acceptable
When to Use / When Not to Use TDE vs Application-Level Encryption
Use TDE when:
- You need baseline protection against physical media theft
- Compliance requires encryption at rest without application changes
- You want minimal operational overhead
Do not use TDE alone when:
- You need column-level access control (TDE decrypts everything for privileged users)
- You need to encrypt only specific fields (use application-layer)
- Regulatory requirements mandate key-per-tenant isolation
Use Application-Level Encryption when:
- You need field-level encryption with separate keys per user or tenant
- Privileged users (DBAs) must not see sensitive data
- You need searchable encrypted data (deterministic encryption)
- Compliance requires key custody separate from data storage
Do not use Application-Level Encryption when:
- Performance overhead is unacceptable and TDE suffices
- Your team lacks secure key management expertise
Encryption Strategy Trade-offs
| Dimension | TDE (Database-Level) | App-Level Column Encryption | TDE + App-Level Combined |
|---|---|---|---|
| Coverage | Entire database | Selected columns only | Entire DB + sensitive columns |
| Privileged user access | Sees plaintext | Stays encrypted | Encrypted (app holds keys) |
| Performance overhead | 5-15% on writes | 10-30% depending on ops | 15-40% combined |
| Key management complexity | Low — DB manages | High — app must manage | Highest — dual key systems |
| Implementation effort | Low | High | Highest |
| Compliance scope | Media theft protection | Access control on data | Defense in depth |
Production Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Key rotation breaking encrypted data | Data permanently unreadable | Test rotation thoroughly, maintain key version history |
| KMS throttling during high write load | Write latency spikes or failures | Cache DEKs locally, implement retry with backoff |
| Backup encryption without key export | Cannot restore to different account | Export key material with backups, test cross-account restore |
| Hardcoded encryption keys in code | Key exposure in version control | Use KMS/Vault exclusively, scan code for secrets |
| TDE without key rotation | Compromised key grants perpetual access | Implement automatic rotation schedule |
Capacity Estimation: Encryption Overhead on I/O Throughput
Encryption at rest adds CPU overhead to every read and write operation. The impact depends on the encryption layer and workload characteristics.
TDE (storage-layer) overhead formula:
effective_throughput = raw_throughput × (1 - encryption_overhead_ratio)
encryption_overhead_ratio ≈ 2-15% for AES-NI hardware acceleration
With AES-NI instructions (modern CPUs), TDE overhead is typically 2-5% for sequential I/O and 5-15% for random I/O due to additional CPU cache pressure. Without AES-NI (older CPUs), overhead can reach 30-50%.
Column-level encryption overhead formula:
per_value_encrypt_time = key_setup_time + block_cipher_time × (value_size / block_size)
per_value_decrypt_time ≈ per_value_encrypt_time × 0.8 # decryption often slightly faster
For a 256-bit AES key with 16-byte block size: encrypting a 100-byte string requires 7 block cipher operations. At 1 microsecond per AES operation, that’s 7 microseconds per value. For bulk operations processing 1M values, this adds 7 seconds of CPU time.
Application-level encryption overhead: Encrypt before writing, decrypt after reading. For a database with 100K reads/second and 100K writes/second, each with 10 encrypted columns: encrypt overhead = 100K × 10 × 7μs = 7 seconds of CPU per second — requiring approximately 7 additional CPU cores.
Key rotation overhead: Re-encrypting data with a new key requires reading all encrypted data, decrypting with old key, re-encrypting with new key, and writing back. For 1TB of encrypted data with 10MB/s re-encryption throughput: 1TB / 10MB/s = 100,000 seconds ≈ 27 hours. Plan key rotation during maintenance windows or use key versioning (encrypt new data with new key, decrypt old data lazily on read) to avoid bulk re-encryption.
Observability Hooks: KMS API Call Monitoring and Key Expiration Alerts
Key metrics: KMS API call latency, KMS throttling events, key rotation timestamps, and encryption operation latency.
-- PostgreSQL with pgcrypto: monitor encryption function call latency
SELECT
query,
calls,
total_exec_time_ms,
mean_exec_time_ms,
max_exec_time_ms
FROM pg_stat_statements
WHERE query LIKE '%pgp_sym_encrypt%'
OR query LIKE '%pgp_sym_decrypt%'
ORDER BY total_exec_time_ms DESC;
# Alert: KMS API latency spike (could indicate KMS throttling)
- alert: KmsApiLatencyHigh
expr: histogram_quantile(0.95, kms_api_latency_seconds) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "KMS API P95 latency {{ $value }}s exceeds 500ms"
# Alert: KMS throttling events
- alert: KmsThrottlingEvents
expr: rate(kms_throttling_total[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "KMS throttling events rate: {{ $value }}/s"
# Critical: Encryption key approaching expiration
- alert: EncryptionKeyExpiring
expr: (key_expiration_timestamp - now()) < 2592000 # 30 days
for: 1h
labels:
severity: critical
annotations:
summary: "Encryption key {{ $labels.key_id }} expires in {{ $value }} seconds"
# Alert: Encrypted volume showing high CPU during encryption operations
- alert: EncryptionCpuOverhead
expr: instance:cpu_encryption_overhead_percent > 15
for: 10m
labels:
severity: warning
annotations:
summary: "Encryption overhead consuming {{ $value }}% CPU on {{ $labels.instance }}"
Quick Recap Checklist
Use this checklist when designing or reviewing encryption at rest for a database system:
- TDE enabled for all production databases
- Sensitive fields use column-level encryption where required
- Encryption keys stored in KMS/Vault, not in application code
- Key rotation scheduled and verified (90-day minimum recommended)
- Backup encryption implemented with keys separate from production
- AES-NI hardware acceleration verified for production systems
- KMS throttling handled via DEK caching
- CloudTrail/Vault audit logs enabled and reviewed
- Encryption overhead benchmarked under realistic write load
- Key access termination procedure documented and tested
Compliance Considerations
Encryption at rest is often mandatory for compliance:
- PCI-DSS: Requires encryption for cardholder data at rest
- HIPAA: Requires encryption for PHI “to the extent feasible”
- GDPR: Encryption is recommended (and sometimes required) for personal data
- SOC 2: Encryption at rest is a common control
Document your encryption strategy for auditors:
- What data is encrypted (and where)
- Encryption algorithms and key lengths
- Key management procedures
- Access controls for key material
- Rotation schedules and procedures
Interview Questions
With AES-NI hardware, TDE overhead should be 2-5%. 30% suggests either old CPUs without AES-NI support or misconfigured encryption settings. Check: grep aesni /proc/cpuinfo on Linux to verify AES-NI is available. Also check if the storage layer is using a weaker cipher for compatibility (3DES in some configurations). If AES-NI is available and configured, 30% overhead points to other factors: possibly the database is now I/O bound rather than CPU bound, and encryption overhead is compounding existing I/O latency. Fix: upgrade to CPUs with AES-NI, use local NVMe storage to reduce I/O latency, or consider moving encryption to a dedicated encryption accelerator card.
Two strategies: lazy re-encryption and blue-green encryption migration. Lazy re-encryption: starting immediately, all new writes use the new key. On reads, decrypt with the key found in the record's metadata. Background process gradually re-encrypts old records as they are accessed. This avoids bulk re-encryption but means some data remains under the old key for months. Blue-green: provision new encrypted storage, migrate new writes to it, run a background re-encryption job during maintenance window, cut over atomically via DNS or proxy. For 10TB with 100MB/s re-encryption throughput: 10TB / 100MB/s = 100,000 seconds ≈ 27 hours. Blue-green requires ~28 hours of maintenance window. Lazy re-encryption is safer but requires key version support in your application layer.
TDE encrypts the entire storage layer — all data, indexes, and logs. Transparent to applications, minimal application changes. Column-level encryption encrypts specific fields before writing to the database. Applications must explicitly encrypt/decrypt. Choose TDE when: you need to protect against storage theft or media theft, you want minimal application changes, compliance requires full-disk or full-database encryption. Choose column-level when: specific fields require encryption (SSN, credit cards, API keys), you need field-level access control (only some users can decrypt), you want to exclude encrypted fields from database indexes (encrypted values are not useful for range queries anyway).
KMS throttling happens when API call rate exceeds KMS limits (default 1000-10000 requests/second depending on region and key type). Fix: implement DEK caching — generate a data encryption key from KMS once, cache it in memory (or Redis for distributed systems), reuse for multiple operations. A DEK can encrypt thousands of data values before rotation. Set cache TTL based on your security requirements (1 hour to 24 hours is common). Also use batch encryption APIs where available (EncryptMany in AWS KMS). For extreme write throughput, consider Vault Transit engine which handles encryption as a service with much higher throughput than cloud KMS.
Evidence depends on your key management system. With AWS KMS: CloudTrail logs every Encrypt, Decrypt, GenerateDataKey operation with timestamp, IAM principal, and source IP. Export CloudTrail logs for the 90-day window, filter for key usage events, and demonstrate no unauthorized access patterns. With HashiCorp Vault: audit logs capture every key operation. Key rotation events prove keys were rotated on schedule. Key version history shows which versions existed and when. Prepare a key access report: list of all key operations, who initiated them, from where, and at what time. Absence of anomalies is the key evidence.
Immediate actions: audit key access logs for the past 6 months to determine if the keys were actually used by the former employee or anyone else. If keys were used, assess what data was decrypted. If the keys were used maliciously, treat it as a breach and follow your breach response procedure (GDPR 72-hour notification may apply if personal data was involved). Technical steps: revoke the former employee's IAM access or Vault token immediately, rotate all encryption keys that the former employee had access to, re-encrypt data with new keys. If no evidence of misuse: document the access, rotate keys as a precaution, and update your key access termination procedure to ensure faster revocation.
TDE protects against physical media theft — if someone steals a drive or database file, they cannot read the data without the encryption key. The thief gets encrypted blobs that are useless without key material. Limitations: TDE does not protect against logical access (if attacker gains access to the running database or backup files with keys, data is exposed), does not protect data in memory (buffer pool is plaintext), and does not protect against queries run by authorized users (encryption is transparent to the database engine). TDE is one layer of defense-in-depth — it handles physical theft scenarios but is ineffective against application-level attacks or insider threats with key access.
Envelope encryption uses a two-tier key hierarchy: data encryption keys (DEKs) encrypt data, and key encryption keys (KEKs) encrypt the DEKs. The KEK lives in KMS/HSM; DEKs live alongside the encrypted data. This pattern enables key rotation without re-encrypting all data — you rotate the KEK, re-wrap DEKs, and the actual data never needs re-encryption. For large-scale deployments with per-record or per-table DEKs, envelope encryption is essential: rotating a master key is fast (only DEKs change), while the bulk data remains accessible. Direct encryption — one master key encrypting all data directly — would require reading, decrypting, and re-encrypting terabytes of data on every key rotation. Envelope encryption is the industry standard for this reason.
FIPS 140-2 mandates specific approved algorithms and implementations. AES-128/256 are approved; DES and 3DES are not. Key derivation functions must use approved algorithms (PBKDF2 with minimum iterations, not simple MD5). Software crypto libraries must be FIPS-certified — OpenSSL's FIPS module is commonly used. Hardware security modules (HSMs) are typically FIPS-certified by design. Implementation impact: you cannot use ChaCha20 (not FIPS-approved), must use TLS 1.2+ with FIPS-approved cipher suites, and KMS/HVault must be configured with FIPS-compliant algorithms. Audit your crypto dependencies — many languages' default crypto libraries include non-approved algorithms.
Zero-downtime rotation uses the dual-key pattern: add a new encryption column alongside the existing encrypted column. New writes use the new key. Background process re-encrypts existing records with the new key, reading with old key and writing to new column. Once re-encryption is complete (verified by checksum), drop the old column and the old key. The process runs without taking the system offline — reads handle both columns during the transition. For 10TB databases, budget 24-72 hours for re-encryption at 100MB/s throughput. Monitor re-encryption progress and failure rates.
Application-managed keys: the database never sees key material (strongest isolation), key management is portable across databases, but key loss means permanent data loss, and the application must handle key distribution. Database-managed keys (via pgcrypto, SQL Server TDE): database handles encryption transparently, simpler application code, but DBA access potentially bypasses encryption. The strongest model: application holds the key, database stores ciphertext. Even DBAs cannot decrypt without the application key. This is essential for compliance when privileged database users must not access sensitive data.
Build a data classification framework: RESTRICTED (SSN, financial accounts, health data — encryption mandatory with key-per-record isolation), CONFIDENTIAL (email, phone, address — TDE or column encryption), INTERNAL (general data — access controls), PUBLIC (no restrictions). Classify by column, not table — a user table contains both restricted and internal data. Create a classification matrix mapping each column to its category, and enforce encryption requirements per category. Audit the classification against actual data flows quarterly. Missing a classification means missing encryption coverage.
Encryption at rest protects against physical media theft — disk theft, backup theft. It does not protect data in memory (buffer pool) or data in use. Confidential computing (Intel SGX, AMD SEV) encrypts data in use — the CPU operates on encrypted memory regions that even OS-level admins cannot access. This closes the gap where encryption at rest is insufficient: a compromised hypervisor or OS cannot read data from an encrypted enclave. AWS Nitro Enclaves, Azure Confidential Computing, and Google Confidential VMs implement this. Encryption at rest + confidential computing = protection across storage, transit, and use.
Bring your own keys (BYOK) or hold your own keys (HYOK): generate keys in your HSM on-premises, transfer the key material to cloud KMS under your control, use the cloud KMS for encryption operations but the key originates from your infrastructure. This satisfies regulatory requirements that keys remain under your control. Implementation: export encrypted key blobs from on-premises, import into cloud KMS, configure database to use cloud KMS for encryption. Some regulations require keys to never leave your infrastructure — in that case, use a hybrid approach with your own HSM and encryption-as-a-service from your infrastructure.
Direct encryption: data encryption key (DEK) encrypts data, DEK is stored alongside the data (or in KMS). Envelope encryption: data is encrypted with a DEK, the DEK is encrypted with a key encryption key (KEK), and the KEK is stored in KMS. Envelope encryption enables key rotation without re-encrypting all data — you rotate the KEK and re-wrap DEKs. Use direct encryption when the data volume is small and key rotation is infrequent. Use envelope encryption when you have many data encryption keys (per-record, per-table) and need to rotate keys regularly without massive re-encryption operations.
Options: let the cloud provider encrypt at rest (default, easiest), encrypt the backup file before uploading (application-layer encryption with your keys), or use cloud-provider key management with BYOK. Best practice: layer encryption — cloud-provider encryption for convenience, application-layer encryption with your own keys for sensitive data. This ensures that even if the cloud provider is compromised, your data remains encrypted with your keys. Never upload plaintext backups to third-party storage without your own encryption layer. Verify that backup files are deleted from storage when retention expires.
Per-tenant key architecture: each tenant has a unique DEK, stored encrypted by a tenant-specific KEK in KMS. The application retrieves tenant keys at runtime (never stored in plaintext). Tenant data is encrypted with tenant's DEK before writing. Key rotation rotates tenant KEKs, re-wrapping tenant DEKs. This ensures that a compromised tenant key affects only that tenant. Database-level TDE alone does not provide tenant isolation — all tenants share the same database encryption key. Only application-layer encryption with per-tenant keys achieves cryptographic tenant isolation.
TDE encrypts data at rest — when the database is running and the key is loaded, data is plaintext in the buffer pool and accessible to anyone with database access. The penetration tester likely connected as a database user and queried data normally. This is expected behavior for TDE — it does not protect against logical access (SQL injection, compromised credentials, privileged user access). TDE + application-layer encryption together provide defense-in-depth: TDE protects physical media, application-layer encryption protects against database-level access. Use the principle of least privilege: users should only access data their application needs, not raw database tables.
Key expiration design: embed a key version identifier in the ciphertext (e.g., v2:base64:...). On read, parse the version, use the corresponding key to decrypt. This allows multiple key versions to coexist during rotation. For renewal: add new key version, update application to use new key for new writes, re-encrypt old data in background (lazy or batch), deprecate old key only when all data is re-encrypted. Key version metadata can be stored in the first bytes of the ciphertext, a database column, or a key registry. Without versioning, key rotation breaks existing ciphertext.
Keys in environment variables are visible in process listings, log files, and core dumps. Remediation: migrate to a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault). The application fetches keys at runtime from the secrets manager, never stored persistently in the application environment. Implement key caching with TTL (short enough to limit blast radius, long enough to avoid performance impact). Audit all access to the secrets manager. Remove hardcoded keys from code, configuration, and version control history — if keys are in git history, treat them as compromised and rotate immediately.
Further Reading
- Cloud Security — Broader security patterns
- Compliance Automation — Maintaining security posture
Conclusion
For further reading on related security topics, see our cloud security guide and explore compliance automation for keeping your security posture up to standards.
Category
Related Posts
Audit Logging: Tracking Data Changes for Compliance
Implement audit logging for compliance. Learn row-level change capture with triggers and CDC, log aggregation strategies, and retention policies.
Data Masking Strategies for Non-Production Environments
Learn static and dynamic data masking: nulling, shuffling, hashing, and range techniques. Understand GDPR and PII considerations for PostgreSQL and Oracle.
GDPR Compliance: Technical Implementation for Database Systems
Understand GDPR requirements: deletion, portability, consent, agreements, breach notification. Database implementation strategies.