Customer-Managed Cloud Encryption Keys: Hierarchy, Rotation, Outage Risk
For developers deciding whether to use customer-managed keys for cloud data encryption, this guide explains the actual key hierarchy, what rotates vs what does not, and why key mistakes cause outages. You will leave with a practical mental model, concrete CLI examples, and a checklist for when CMKs are worth the operational burden.
TL;DR — Customer-managed keys (CMKs) usually do not encrypt every object directly; they wrap short-lived data-encryption keys, and that key hierarchy is what makes cloud-scale encryption practical. The biggest real risk is not cryptography failure but availability: disabling, deleting, mis-permissioning, or aggressively rotating a CMK can instantly break reads, writes, startup, restores, and autoscaling across dependent services. Reading time: ~7 min
What it is and where it sits
When you enable customer-managed encryption for cloud storage, databases, queues, or disks, you are inserting your own key material or key policy into the provider's encryption path. In most systems, the provider already encrypts data at rest with provider-managed keys. CMK replaces only the top-level trust anchor, not the entire encryption implementation.
Architecture-wise, the important point is this: your app usually does not call the CMK service on every row read or object GET. The storage service does. Your app talks to the storage API; the storage service talks to the key management service (KMS/HSM-backed service) to unwrap a data key; the storage service then encrypts/decrypts data internally.
That means CMK sits on the control path for key access and on the data path only indirectly.
View as text
[Client] ---- 1. PUT /invoice.pdf ----> [App] [App] ---- 2. Store object ----> [Object Storage] [Object Storage] ---- 3. Generate/unwrap DEK ----> [KMS/CMK] [KMS/CMK] <---- 4. Wrapped DEK / plaintext DEK ---- [Object Storage] [Object Storage] | v [Encrypted object + encrypted DEK + metadata]
What it replaces in a typical flow:
- Without CMK: storage service uses provider-managed root keys you cannot control much.
- With CMK: storage service still generates per-object or per-volume data encryption keys (DEKs), but wraps them with a key encryption key (KEK) rooted in your CMK.
What talks to it:
- Managed services: object storage, block volumes, managed databases, message queues, backup systems.
- Sometimes your app directly, if you use envelope encryption in application code.
- IAM/policy systems, because key usage is gated by permissions and conditions.
Where it lives:
- In the cloud provider's KMS or HSM-backed key service.
- Sometimes backed by imported key material or external key manager integration.
- Always coupled to IAM, audit logs, and service identities.
The practical consequence: if the CMK becomes unavailable, your application code may still be healthy, but the managed service can no longer unwrap DEKs. That looks like a storage outage, database startup failure, or backup restore failure.
How it actually works
Take one realistic example: an app uploads a PDF to object storage with server-side encryption using a customer-managed key.
Step-by-step flow
- Your app sends
PUT /bucket/invoice-8472.pdfwith encryption headers or bucket-level default encryption already configured. - Object storage asks KMS for a fresh DEK, typically via a "generate data key" operation under your CMK.
- KMS returns:
- a plaintext DEK for immediate use
- the same DEK encrypted under the CMK
- Object storage encrypts the PDF with the plaintext DEK, usually with AES-GCM or equivalent authenticated mode.
- Object storage discards the plaintext DEK from memory after use.
- Object storage stores:
- ciphertext object bytes
- encrypted DEK blob
- metadata saying which CMK/version was used
- Later, on
GET, object storage reads the encrypted DEK blob, calls KMSDecrypt/unwrap under the CMK, gets the plaintext DEK back, decrypts the object, and streams plaintext to the caller over TLS.
What rotates, concretely
There are three different things people call "the key," and confusing them causes bad decisions:
- CMK / KEK: the customer-managed root or wrapping key in KMS.
- DEK: per-object, per-file, per-volume, or per-chunk data key.
- Key version: a new cryptographic version of the CMK after rotation.
In a healthy design:
- DEKs rotate frequently or are generated per object automatically.
- CMKs rotate infrequently, often on a schedule like 90-365 days depending on policy.
- Old CMK versions remain enabled for decrypting old DEKs.
That last point is the one teams miss. Rotation is usually not "reencrypt all data immediately with the new CMK version." Rotation usually means "new writes use the new CMK version; old data remains decryptable because old key versions are retained."
Why outages happen
Outages happen when teams treat key rotation like certificate rotation and remove the old key version too early, or when they disable/delete the CMK assuming encrypted data has already been "migrated."
Typical failure modes:
- Key disabled: reads and writes start failing within seconds or minutes.
- IAM policy changed: service principal can no longer call
DecryptorGenerateDataKey. - Region mismatch: service in region A cannot use key in region B.
- Restore path forgotten: backups/snapshots encrypted under old CMK cannot be restored.
- Autoscaling surprise: new instances cannot attach encrypted volumes because boot-time decrypt fails.
Realistic error shapes vary by provider, but they usually look like one of these classes:
AccessDeniedException: User/service is not authorized to perform kms:Decrypt on key arn:...
KMSInvalidStateException: Key state is Disabled
FailedPrecondition: encryption key is not enabled
PermissionDenied: caller does not have permission to use this key
If your app hits a managed service, you may only see a generic 500/503 while the service logs contain the key error:
$ curl -i https://api.example.com/files/8472
HTTP/1.1 503 Service Unavailable
content-type: application/json
retry-after: 30
{"error":"storage backend unavailable"}
The storage service audit trail is where the real diagnosis appears: repeated decrypt failures against the CMK.
When to use it (and when not to)
Use CMK when you need control over key lifecycle, auditability, separation of duties, or contractual/regulatory proof that the provider cannot unilaterally manage the root key without your policies.
Do not use it just because "more security" sounds good. For many workloads, provider-managed encryption is already the right default because it removes a whole outage class.
| Scenario | Recommendation |
|---|---|
| You need auditable key usage logs and the ability to disable access on demand | Use CMK |
| You need per-tenant crypto isolation for high-value tenants | Use CMK, often one key per tenant tier or per data domain |
| You run regulated workloads requiring customer control of key policies or imported key material | Use CMK |
| You have a small team, no 24/7 ops, and standard app data in managed storage | Probably use provider-managed keys |
| You cannot tolerate self-inflicted storage/database outages from IAM or key state changes | Avoid CMK unless the requirement is explicit |
| You plan to rotate by deleting old keys quickly for "security" | Do not use CMK until the team understands version retention and re-encryption |
| You need app-level cryptographic separation from the cloud provider | CMK alone is not enough; consider application envelope encryption |
You probably don't need this if:
- your only reason is a generic compliance checkbox and provider-managed encryption already satisfies it
- you do not have inventory of which services, backups, and snapshots depend on which key
- nobody on the team owns KMS policy changes and break-glass recovery
Trade-offs
Every benefit here has a bill.
-
Benefit: stronger control over key access and audit logs
Cost: more IAM policy surface area, more ways to break production with a bad deny, region mismatch, or deleted grant. -
Benefit: easier separation of duties
Cost: developers, platform engineers, and security now need coordinated rollout steps. Deploys can fail because key policy lagged behind infrastructure creation. -
Benefit: faster crypto revocation in an incident
Cost: revocation is also a self-DoS button. Disabling a CMK can stop reads, writes, scaling, restores, and even log delivery. -
Benefit: customer ownership story for contracts/regulation
Cost: direct KMS charges, indirect latency, and operational runbooks. The money is usually not the biggest issue; the human burden is. -
Benefit: key rotation under your policy
Cost: rotation planning, old-version retention, and occasional bulk re-encryption jobs if policy requires cryptographic refresh of historical data. -
Benefit: tenant or domain isolation
Cost: key sprawl. Hundreds or thousands of keys means tagging, aliasing, policy templating, quotas, and incident triage all get harder.
Latency note: for managed services, KMS calls are often cached or amortized, so the per-request latency hit may be small. But on cold start, restore, mount, or first access after cache expiry, KMS availability and latency become visible. Treat KMS as a dependency for your storage plane.
Lock-in note: once snapshots, backups, and objects are encrypted under a provider-specific KMS integration, migration gets harder. You need a data copy or re-encryption plan, not just an export.
In practice
Example 1: Encrypt object uploads with a CMK from the CLI
aws s3api put-object \
--bucket invoices-prod \
--key 2026/10/invoice-8472.pdf \
--body ./invoice-8472.pdf \
--server-side-encryption aws:kms \
--ssekms-key-id arn:aws:kms:us-east-1:111122223333:key/1234abcd-56ef-78ab-90cd-1234567890ef
This uploads one object and tells object storage to use a specific CMK. Gotcha: the caller needs permission to write the object, and the storage service principal also needs permission to use the key; if either side is missing, you will get an access error that looks like storage failure, not a crypto failure.
Typical failure output shape:
An error occurred (AccessDenied) when calling the PutObject operation:
Access Denied
And in audit/KMS logs you will usually find the real reason:
AccessDeniedException: not authorized to perform kms:GenerateDataKey on resource arn:aws:kms:...
Example 2: Application envelope encryption with a KMS-wrapped DEK
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64, os, json
# Pseudocode: replace kms_generate_data_key / kms_decrypt with your provider SDK calls.
def encrypt_blob(plaintext: bytes, context: dict):
dek_plaintext, dek_ciphertext = kms_generate_data_key(
key_id="projects/acme/locations/us/keyRings/app/cryptoKeys/customer-data",
aad=context,
num_bytes=32,
)
nonce = os.urandom(12)
aesgcm = AESGCM(dek_plaintext)
ciphertext = aesgcm.encrypt(nonce, plaintext, json.dumps(context).encode())
return {
"ciphertext": base64.b64encode(ciphertext).decode(),
"nonce": base64.b64encode(nonce).decode(),
"wrapped_dek": base64.b64encode(dek_ciphertext).decode(),
"context": context,
}
def decrypt_blob(blob: dict):
dek_plaintext = kms_decrypt(
key_id="projects/acme/locations/us/keyRings/app/cryptoKeys/customer-data",
ciphertext=base64.b64decode(blob["wrapped_dek"]),
aad=blob["context"],
)
aesgcm = AESGCM(dek_plaintext)
return aesgcm.decrypt(
base64.b64decode(blob["nonce"]),
base64.b64decode(blob["ciphertext"]),
json.dumps(blob["context"]).encode(),
)
This is the same hierarchy managed services use internally: DEK for data, CMK for wrapping. Gotcha: keep the encryption context/AAD stable; if you change tenant ID, object path, or JSON serialization order between encrypt and decrypt, decrypt will fail even though the key is correct.
Example 3: Rotation runbook check before disabling an old key version
⚠️ Disabling or scheduling deletion of a CMK or old key version can cause immediate production outages and can permanently strand backups if no decrypt path remains. Run these checks before touching key state.
# 1) Inventory live dependencies by tag/alias naming convention
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=cmk-alias,Values=customer-data-prod
# 2) Query recent KMS usage in your audit system for Decrypt/GenerateDataKey
# Replace with your provider's audit query tool.
audit query '
service="kms" AND
key_id="arn:aws:kms:us-east-1:111122223333:key/1234abcd-56ef-78ab-90cd-1234567890ef" AND
event_name IN ("Decrypt","GenerateDataKey","Encrypt") AND
timestamp > now() - 30d
'
# 3) Test restore/read path in a non-prod account/project using a recent backup/snapshot
restore-tool restore --snapshot snap-0abc1234 --target staging-restore
This is not glamorous, but it is the difference between safe rotation and a sev-1. Gotcha: production may have hidden dependencies like old snapshots, asynchronous replication, search indexes, or logs encrypted under the same key.
Practical rotation policy
A sane default policy for most teams:
- rotate CMKs on a fixed schedule supported by your provider
- keep old key versions enabled for decrypt indefinitely or until all dependent data is re-encrypted and restore-tested
- use aliases/logical names in config, not hard-coded version IDs
- separate keys by data domain, not by every microservice unless you truly need that blast-radius split
- alert on key state changes, scheduled deletion, and decrypt-denied events
Further reading
- NIST SP 800-57 Part 1: Key Management Guidelines
- OWASP Cryptographic Storage Cheat Sheet
- The "Envelope encryption" sections of major cloud KMS documentation
- The audit logging section of your cloud provider's KMS docs
- Designing Data-Intensive Applications, chapters on storage and reliability
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI