Data security and KMS
Encryption at rest is only as strong as who can call Decrypt. KMS-backed envelope encryption and a deny-by-default key policy are what actually protect data — not the AES box being ticked.
The breach review is over and the verdict is the same one you’ve seen three times this year: the bucket was encrypted. “Server-side encryption with KMS” was ticked, the compliance checkbox was green, the auditor signed off. And yet 40 million customer records walked out the door in plaintext. The attacker never touched a ciphertext. They assumed an over-broad IAM role, called kms:Decrypt, and the KMS service — doing exactly its job — handed back the data key and decrypted everything for them. Encryption at rest didn’t fail. It worked perfectly, for the wrong principal. The control that mattered was never the cipher. It was the key policy — and nobody had read it.
By the end of this lesson you’ll know why “encrypted at rest” is a near-meaningless guarantee on its own, how envelope encryption with a KMS actually moves the security boundary onto an access-control decision, and how to write a key policy that makes Decrypt the hard part instead of the free part.
What “encryption at rest” actually buys you
Start with the uncomfortable truth: server-side encryption at rest defends against exactly one threat — someone physically walking off with the disk, or reading the raw storage medium out from under the running system. That’s it. A stolen drive, a decommissioned SSD that wasn’t wiped, a snapshot copied to the wrong account by a tired engineer: against those, at-rest encryption is genuine and valuable. It is the floor.
It does nothing against the threat that actually happens. When your application reads a row, the storage layer transparently decrypts it first — that’s the entire point of transparent encryption. So any principal that can make your service read the data, or can call the decrypt API directly, gets plaintext. SQL injection returns plaintext. A leaked database credential returns plaintext. An over-privileged IAM role calling kms:Decrypt returns plaintext. The encryption is invisible to the attack because it’s invisible to every legitimate read, and the attacker is riding a legitimate read path. “Is the data encrypted at rest?” is the wrong audit question. The question is “who can cause it to be decrypted?” — and that answer lives in IAM and the key policy, not in the storage config.
This reframing is the whole lesson. Encryption at rest converts a data-confidentiality problem into a key-access-control problem. That’s a good trade — access control is something you can reason about, audit, log, and revoke — but only if you treat the key access as the real security boundary and not an afterthought you inherited from a default.
Envelope encryption: why nobody encrypts data directly with KMS
You cannot send a 4 GB file to a KMS to encrypt. Hardware-backed key services cap their direct encrypt payload at a few kilobytes (AWS KMS: 4 KB) because the master keys never leave the HSM — every byte you want encrypted would have to make a network round-trip into the boundary and back. So the entire industry uses envelope encryption, and understanding its two-key structure is what makes the rest of this make sense.
There are two layers of key:
- The data key (DEK) — a fresh symmetric key (e.g. AES-256) generated per object or per logical scope. It encrypts the actual bytes, locally, at full speed. This is the key that does the bulk work.
- The key-encryption key (KEK), also called the master key or CMK — a long-lived key that never leaves the KMS/HSM. Its only job is to encrypt and decrypt data keys.
The flow: you ask KMS to generate a data key. It hands you that key twice — once in plaintext (use it now, in memory) and once already encrypted under the KEK (the “wrapped” or “encrypted data key”). You encrypt your payload with the plaintext DEK, then immediately discard the plaintext DEK from memory and store the wrapped DEK right next to the ciphertext. To read the data back, you send the wrapped DEK to KMS, KMS calls Decrypt using the KEK it holds, returns the plaintext DEK, you decrypt the payload, and discard the DEK again.
Why this structure earns its complexity: the bulk data never crosses the KMS boundary, so you keep AES-at-line-speed performance; the only thing KMS ever touches is the tiny wrapped DEK. And it gives you two independent revocation levers. You can rotate the KEK without re-encrypting a single byte of data — only the small wrapped DEKs need re-wrapping. And because the plaintext DEK only exists transiently in application memory, the durable security of all your data reduces to the durable security of one thing: access to call Decrypt on the KEK. That collapse — petabytes of confidentiality resting on one access-control decision — is exactly why the key policy is the real artifact.
▸Why this works
Why generate a new data key per object instead of reusing one fast local key for everything? Blast radius and crypto hygiene. A single DEK encrypting millions of records is a single point of compromise: leak it once (a heap dump, a swapped-out page, a log of decrypted material) and every record it ever touched is exposed, with no way to revoke just one. Per-object (or per-tenant) DEKs mean a leaked plaintext DEK exposes only its own scope, and you can crypto-shred a single tenant by destroying their wrapped DEK — the ciphertext becomes permanently unrecoverable noise without ever touching the other tenants’ data. It also keeps each individual key well under the data-volume limits that weaken AES-GCM nonce uniqueness at scale.
The key policy is the security control
Here is the part teams skip, and the reason the Hook breach keeps recurring: the key policy and the IAM permissions around kms:Decrypt are the encryption-at-rest control. The cipher is commodity; the access decision is the product.
A sane key policy is deny-by-default and least-privilege over the key, the same discipline you’d apply to any resource — just applied to the one resource that gates everything else:
- Scope
Decryptto the smallest set of principals that genuinely need plaintext. The ingestion service that writes encrypted rows often needs onlyGenerateDataKey(to make new DEKs) and notDecryptat all — it never reads back. The analytics job might needDecryptbut only via a specific role. Most human users and most services should appear nowhere in the key policy. - Constrain with conditions, not just principals. Use encryption context (additional authenticated data) so a wrapped DEK for tenant A literally cannot be decrypted under a request claiming tenant B — the
Decryptcall fails because the AAD doesn’t match. Bind grants to source VPC, to a specific role session, to a specific resource. A bareAllow Decrypton*is the policy that lost 40 million records. - Separate the key administrators from the key users. Whoever can call
Decryptshould not also be able to rewrite the key policy to grant themselves more — and the person who can change the policy shouldn’t silently be able to read data. This is separation of duties applied to the key, and it’s what stops a single compromised role from both reading data and erasing the evidence that it could.
The senior reflex: when you see “encrypted with KMS,” your next click is the key policy, and you read it the way you’d read an access-control list — asking which principals can turn ciphertext back into plaintext, under what conditions, and who can change that answer. If the policy is the AWS default (root account + broad admin), the data is effectively protected by your general IAM hygiene and nothing more specific — the encryption is doing PR, not defense.
| Threat | At-rest encryption alone | What actually stops it |
|---|---|---|
| Stolen physical disk / unwiped SSD | Stops it — this is the one threat it’s for | The at-rest encryption itself |
| Snapshot copied to wrong account | Stops it only if the target account can’t use the KEK | Key policy scoped to source account |
| Over-privileged role calls kms:Decrypt | No protection — KMS decrypts for it | Least-privilege key policy + conditions |
| SQL injection / leaked DB credential | No protection — read path auto-decrypts | App-layer authz; encryption is the wrong tool |
| Need to permanently destroy one tenant’s data | Hard — must overwrite every record | Crypto-shred: destroy that tenant’s wrapped DEK |
Rotation, shredding, and the failure modes
Two operational properties fall out of the two-key design, and both are routinely misunderstood.
Rotation. Automatic KEK rotation (e.g. annual) does not re-encrypt your data and does not re-wrap your stored DEKs — it adds a new KEK version and uses it for new wrap operations, while keeping old versions available to unwrap existing DEKs. This is why rotation is cheap and why people who expect it to “re-protect old data” are wrong: a DEK wrapped under the 2023 KEK version is still unwrappable forever unless you actively re-wrap it. True key destruction (deleting a KEK version) is the only thing that renders its wrapped DEKs — and thus the underlying ciphertext — permanently unrecoverable. That’s the mechanism behind crypto-shredding: to make data unrecoverable on demand (a GDPR erasure request, a decommissioned tenant), you don’t scrub petabytes — you destroy the small wrapped DEK that’s the only path to that data, and the ciphertext becomes irreversible noise instantly.
The failure modes to respect:
- Losing or destroying a KEK you still needed is unrecoverable data loss by design — there is no recovery, no support ticket, no backdoor. The same property that makes crypto-shred powerful makes an accidental key deletion catastrophic. AWS enforces a mandatory 7–30 day waiting period on key deletion precisely because the operation is irreversible.
- The
Decryptcall is a hard runtime dependency. If KMS is unreachable, or you cross a region where the key doesn’t exist, or you hit the per-key request-rate limit during a traffic spike, your reads start failing — the data is intact but un-decryptable until KMS answers. Envelope encryption trades a confidentiality guarantee for an availability dependency on the key service, and you must plan for KMS throttling and regional key availability the way you’d plan for any critical dependency. - Forgetting to discard the plaintext DEK turns the whole scheme into theatre. If the plaintext DEK lingers in a log, a heap dump, a cache, or a crash artifact, an attacker who reads it skips KMS entirely. The wrapped DEK is safe to store anywhere; the plaintext DEK is radioactive and must live only transiently in memory.
Tie it back to detection and posture: a key policy is a static artifact you can scan (posture), and Decrypt calls are events you can monitor (detection). NIST SP 800-137’s continuous-monitoring model applies directly — an anomalous spike in kms:Decrypt from a role that normally only writes, or a Decrypt whose encryption context doesn’t match the expected tenant, is one of the highest-signal alerts you can wire up, because by the design above it sits exactly on the one boundary that matters.
A compliance scan reports 'S3 bucket encrypted with KMS — PASS.' The bucket holds customer PII. As the security reviewer, what's your next action?
Why does envelope encryption use a per-object data key (DEK) wrapped by a master key (KEK) instead of encrypting the data directly with the KMS master key?
A storage bucket is encrypted at rest with KMS. An attacker steals a valid database credential and queries the data through the normal read path. What do they get?
Order the envelope-encryption write path, from requesting a key to safely storing the result:
- 1 Call KMS GenerateDataKey for the KEK
- 2 Receive the DEK twice: plaintext + KEK-wrapped
- 3 Encrypt the payload locally with the plaintext DEK
- 4 Discard the plaintext DEK from memory
- 5 Store the ciphertext beside the wrapped DEK
- 01Explain why 'the bucket is encrypted at rest with KMS' tells you almost nothing about whether the data is actually protected, and what you should look at instead.
- 02Walk through envelope encryption and explain what it buys you over encrypting data directly with a KMS master key.
Encryption at rest defends against one narrow threat — someone reading the raw storage medium — and nothing else, because every legitimate read path transparently decrypts the data before your application sees it. That means a leaked credential, SQL injection, or an over-broad IAM role calling kms:Decrypt all walk away with plaintext while the “encrypted” checkbox stays green. Envelope encryption is how you turn this into something defensible: a per-object data key (DEK) encrypts the bulk data locally at full speed, that DEK is wrapped by a master key (KEK) that never leaves the HSM, and the wrapped DEK is stored beside the ciphertext. The whole scheme’s security then collapses onto one access-control decision — who can call Decrypt on the KEK — which is why the key policy, scoped deny-by-default and least-privilege with conditions like encryption context and separation of key admins from key users, is the actual control, not the cipher. The two-key design also gives you cheap KEK rotation (no data re-encryption) and crypto-shredding (destroy one wrapped DEK to render exactly that data unrecoverable), at the cost of making Decrypt a hard availability dependency and making an accidental key deletion irreversible. So the next time a scan says “encrypted with KMS — PASS,” your first move is to open the key policy and ask: which principals can turn this ciphertext back into plaintext, under what conditions, and who can change that answer?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.