Frontend Labs
⋯

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)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. What does storage-layer/TDE encryption protect against, and what does it not protect against?
  2. Why and when do you add application-layer field encryption on top?
  3. What is a blind index and what trade-off does it make versus deterministic encryption?
  4. Where should decryption happen, and where should it never happen?
  5. 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.

Generated from real-life-security-guide-v1.md · 8 Oct 2026 16:36