Chapter 42 — Encryption at rest, done properly
In one sentence
"Encrypted at rest" is meaningless without answering who can decrypt, under what conditions, and what does that protect against — which is why real data protection uses a key hierarchy, decides deliberately between transparent and application-layer encryption, and treats key access as the actual authorization control.
Where we are
Part 2 Ch 9 built envelope encryption with cloud KMS. This chapter applies it at the data layer — inside the database, per field, per tenant — and is explicit about what each encryption choice does and doesn't defend against.
How it works
What "encrypted at rest" actually protects, by layer:
FULL-DISK / STORAGE-LAYER ENCRYPTION (EBS, RDS storage encryption,
TDE-at-the-storage-engine)
protects against: a stolen physical disk, an improperly decommissioned
drive, a snapshot copied to the wrong place, a cloud provider's
storage layer being compromised (defence in depth for THEM).
does NOT protect against: the running database process reading data
(it decrypts transparently), a compromised app with valid DB
credentials, a malicious DBA with query access, a SQL injection
that reads the table. If the app can always ask the DB for
plaintext, disk encryption is invisible to that threat.
TRANSPARENT DATA ENCRYPTION (TDE) -- SQL Server/Oracle/some Postgres
extensions
same threat model as storage-layer: the engine holds the key and
decrypts for any authenticated query. Convenient, zero app changes,
but doesn't narrow "who can read this column" beyond DB access
control.
APPLICATION-LAYER / FIELD-LEVEL ENCRYPTION
the APPLICATION encrypts specific fields before they ever reach the
database, using a key the database itself never has.
protects against: EVERYTHING above, PLUS a compromised database
server, a malicious DBA, a misconfigured DB-wide grant, a backup
exfiltrated whole, an ORM bug that returns more columns than
intended, a BYPASSRLS query (Part 5 Ch 27).
costs: you lose native SQL operations on that field (can't `WHERE
email = ?` directly, can't sort/range-query, indexing needs care)
unless you add blind indexing (below); more engineering; key
management is now your job, not the DB engine's.
THE RULE: use storage-layer/TDE as the baseline for everything (cheap,
no downside). Add APPLICATION-LAYER encryption for the specific fields
where the threat model includes "the database itself is
compromised, or a DBA/analyst with DB access is the threat" -- which,
for RESTRICTED data (Ch 41), it should.
Field-level encryption, built on envelope encryption (Part 2 Ch 9):
PER-FIELD:
ciphertext = AES-256-GCM(plaintext, DEK, nonce, AAD=field_name+
record_id+tenant_id)
DEK wrapped by a PER-TENANT CMK (Part 2 Ch 9) -- offboarding =
crypto-shred that tenant's key (Ch 46).
WHERE DOES DECRYPTION HAPPEN?
- in the application service that legitimately needs the plaintext
(most common; the service holds the KMS permission).
- in a dedicated CRYPTO SERVICE / sidecar that every other service
calls via an API ("encrypt this", "decrypt this") -- centralises
key access, audit, and rotation logic; other services never touch
KMS directly. Good when many services need the same fields.
- NEVER at the DB proxy transparently for everyone -- that
re-creates the TDE threat model with extra steps.
SEARCHING ENCRYPTED FIELDS -- the real trade-off:
- DETERMINISTIC encryption (same plaintext -> same ciphertext,
always) enables equality lookups (`WHERE email_enc = ?`) but LEAKS
equality and frequency (an attacker with ciphertext access learns
which rows share a value, and can build a frequency-analysis
dictionary for low-entropy fields).
- BLIND INDEXING: store an HMAC (keyed, NOT reversible) of the
normalised plaintext alongside the (randomised, non-deterministic)
ciphertext. Query by computing the same HMAC and matching the
index column. Leaks less than deterministic encryption (no
frequency info if you also add per-tenant HMAC keys) but still
permits equality-only lookup; range/partial queries are not
possible without leaking more (see searchable encryption research,
and Format-Preserving Encryption, Ch 43, for a different trade-off).
- Simplest and often right: DON'T index/search the encrypted field at
all; look up by a non-sensitive key (user_id) and decrypt after
fetching the row.
What must never be plaintext, ever, regardless of the above: passwords (hashed, not encrypted, G1 Ch 10), CVV/CVC (never stored at all, G1 Ch 50), and raw payment card numbers where tokenization (Ch 43) is available instead of encryption.
Worked example
Storage encryption stops a stolen snapshot; field encryption stops a compromised analyst account — showing why you need both.
SCENARIO A: an engineer accidentally shares an RDS snapshot publicly
for four hours (a real, recurring incident class).
- Storage-layer encryption (RDS encrypted storage): the snapshot
is encrypted with the DB's CMK. Whoever finds the shared snapshot
cannot restore/read it without kms:Decrypt on that key -> the
accidental exposure is a near-miss, not a breach, PROVIDED the key
policy doesn't grant broad access (Part 2 Ch 9). Storage encryption
did its job here.
SCENARIO B: a support/analytics contractor's account is phished; the
attacker has the SAME DB read access that contractor legitimately had
(a broad `SELECT` grant for support lookups).
- Storage-layer encryption is invisible to this attacker -- the DB
engine decrypts transparently for any authenticated query. They
run `SELECT email, address, phone FROM users` and get everything
in plaintext.
- WITH field-level encryption on `email`/`address`/`phone` (keys held
by the app's crypto service, NOT grantable via a DB role): the
same query returns ciphertext. The compromised contractor account
gets nothing usable for those fields. Support tooling that
legitimately needs to SEE a customer's email calls the crypto
service with ITS OWN identity and an authorization check (Part 5
Ch 25) -- not a blanket DB grant.
Neither control alone covers both scenarios. Storage encryption is the
free baseline; field-level encryption is the deliberate choice for
data where "the database/DBA/analyst is the threat" is in your model.
Practice (75 min)
- Confirm storage-layer encryption is on for every data store in a real account (RDS, EBS, S3 default encryption) using a customer-managed CMK (Part 2 Ch 9), not the provider default key. Fix any gaps.
- Implement application-layer field encryption for one column: envelope
encryption with a per-tenant DEK, AAD binding to
field+record_id+tenant_id, decryption only in the service that needs it. - Add a blind index: an HMAC-keyed column for equality lookup on the encrypted field; query by it; confirm you cannot get equality results without knowing the HMAC key, and that ciphertext for the same plaintext differs across rows (non-deterministic encryption) while the blind index still matches.
- Simulate Scenario B: grant a "support" DB role broad
SELECT; show it returns ciphertext for the field-encrypted columns; show the app's authorized support tool (calling the crypto service, with its own authz check) can still see the real value for a specific customer. - Simulate Scenario A: create a snapshot, "share" it (in the sandbox), and confirm restoring it elsewhere fails without the CMK permission.
Common mistakes
- "We encrypt at rest" as the whole answer. Ask which layer and who can decrypt. Storage-layer alone doesn't defend against a compromised app or DB account.
- Field-level encryption on everything. Costly and unnecessary; reserve it for RESTRICTED data where the DB/DBA is plausibly the threat.
- Deterministic encryption on high-value, low-entropy fields (SSNs, DOB, status flags) without understanding the frequency-leak trade-off.
- Decryption available to any service that asks. Centralise through a crypto service or scope KMS grants tightly (Part 2 Ch 6/9); "decrypt" should be as governed as "read the plaintext" because it is.
- Provider default (AWS-managed) keys for RESTRICTED data. You lose the ability to answer "who can decrypt this" precisely and can't crypto-shred (Ch 46).
- Forgetting backups and read replicas. Field-level ciphertext travels with the data automatically (good); confirm storage-layer encryption is independently verified on every replica/backup destination too.
Check yourself
- What does storage-layer/TDE encryption protect against, and what does it not protect against?
- Why and when do you add application-layer field encryption on top?
- What is a blind index and what trade-off does it make versus deterministic encryption?
- Where should decryption happen, and where should it never happen?
- Using the two-scenario worked example, explain why you need both layers.
Further reading
- Reference: AWS "Database Encryption at Rest" whitepaper; the AWS Encryption SDK docs (correct envelope encryption + AAD, don't hand-roll it).
- Book: Practical Cloud Security (2nd ed.), Ch. on data protection; Serious Cryptography (2nd ed.), the AEAD and key-management chapters (G1 further reading) applied here.
- Searchable encryption: CipherSweet (PHP, blind-indexing library, good reference implementation and docs on its trade-offs); the "Practical Techniques for Searches on Encrypted Data" literature for the theory.
- Standard: NIST SP 800-57 (key management) Part 1, general guidance that underlies this chapter and Ch 44.