Engineering Atlas › System Design Fundamentals

Protecting stored data with encryption at rest

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Layers

  1. Full disk or volume encryption: the storage layer encrypts every block. Transparent to apps, protects against lost or decommissioned hardware. PostgreSQL, for example, has no built-in transparent data encryption and points to file-system or block-level encryption such as dm-crypt with LUKS, noting that this gives no protection while the file system is mounted (PostgreSQL docs).
  2. Database or storage service encryption (TDE, server-side object encryption): the engine encrypts files or objects with keys from a KMS. Protects backups and snapshots, still transparent to queries. This layer is now the floor rather than an option on the big clouds: since 5 January 2023 Amazon S3 encrypts every new object with SSE-S3 (AES-256) and the setting cannot be turned off (AWS S3 docs), and Google Cloud encrypts all customer content at rest by default (Google Cloud). The design decision left to you is who controls the keys.
  3. Application-level or field-level encryption: the app encrypts specific values before storing them. The database only ever sees ciphertext. Protects against database-level access, but you lose the ability to index, sort, or search those fields without extra techniques, such as a keyed HMAC column for exact-match lookup or a purpose-built scheme like MongoDB's Queryable Encryption, which supports equality, range, prefix, suffix and substring queries on encrypted fields (MongoDB docs).

Envelope encryption

The app asks the KMS for a data encryption key (DEK). The KMS returns the DEK in plaintext and also wrapped under a key encryption key (KEK) that stays inside the KMS. The app encrypts data locally with the DEK using an AEAD cipher such as AES-256-GCM, stores the ciphertext next to the wrapped DEK, and discards the plaintext DEK. To read, it sends the wrapped DEK to the KMS, gets the DEK back if authorised, and decrypts locally.

Rotation

Rotating the KEK creates a new key version. New wraps use the new version; old wrapped DEKs still decrypt because the KMS keeps old versions. AWS KMS rotates customer managed keys yearly by default when rotation is on, supports on-demand rotation, and has rotated AWS managed keys every year since May 2022, down from every three years; it is explicit that rotation does not re-encrypt data or rotate data keys, so it does nothing for a leaked DEK (AWS KMS docs). Re-wrapping existing DEKs under the new version is cheap because only the 32-byte key is touched, not the data. Rotating DEKs requires re-encrypting data, which is expensive and usually done only after a suspected compromise.

Authenticated encryption and context

AES-GCM produces a tag that detects tampering. Binding additional authenticated data such as the tenant id and record id means a ciphertext copied from one row to another fails to decrypt, stopping swap attacks. GCM needs a unique nonce per encryption under the same key; with random 96-bit nonces, keep each key to well under 2^32 encryptions.

Crypto-shredding

If each tenant or user has its own DEK, deleting that key makes all their data unreadable, including in backups. This is a practical way to meet deletion obligations without rewriting archives.

Down the stack

Layer 1The contract

A KMS promises that Decrypt returns plaintext only if the caller's identity is allowed by the key policy and the supplied encryption context matches the one used at encryption, byte for byte. It promises that every call is logged and that key material never leaves its HSMs in plaintext. AES-GCM promises confidentiality and integrity as long as a (key, nonce) pair is never reused; with random 96-bit nonces, NIST requires that the total number of encryptions under one key not exceed 2^32 (NIST SP 800-38D, section 8.3). NIST announced in 2024 that it will revise SP 800-38D, so check for the newer edition before quoting limits in a design review. Disk encryption promises much less: confidentiality of sectors at rest, no integrity.

Layer 2The mechanism

GenerateDataKey returns two fields: Plaintext and CiphertextBlob. The blob embeds the key id and version, so Decrypt does not need to be told which key to use. The encryption context is bound as additional authenticated data. On Linux, dm-crypt with LUKS encrypts each sector with AES-XTS, using the sector number as the tweak. The LUKS header holds the volume master key wrapped by one or more key slots, each derived from a passphrase or token with a slow KDF. Cloud block storage uses a per-volume data key wrapped by a KMS key; the host unwraps it at attach time and keeps it in the hypervisor or offload card, never in the guest. MySQL InnoDB stores a per-tablespace key in the tablespace header, wrapped by a master key from a keyring plugin.

Layer 3The machine

With AES-NI, AES-256-GCM runs at roughly 4 to 8 GB/s per core and XTS is similar, so encryption rarely limits a disk that does 3 to 7 GB/s, though it shows up as CPU at very high IOPS. XTS is deterministic per sector: the same plaintext at the same sector produces the same ciphertext, so an attacker with snapshots over time can see which sectors changed. A 4 KB sector has no spare room for a tag, so disk encryption cannot detect tampering. Plaintext keys live in RAM, where core dumps, swap, and cold-boot attacks can expose them, so key-handling processes disable core dumps and mlock key pages. A KMS call is an HTTPS request of 5 to 20 ms, and HSMs handle thousands, not millions, of operations per second.

Why it's built this way

HSMs are slow and scarce, so they wrap small keys and bulk data is encrypted locally. Sectors are fixed size, so disk encryption uses a tweakable mode without integrity, and authenticated encryption has to happen higher up. Insiders and compromised database credentials sit above the disk layer, so only application-level encryption stops them. Immutable backups cannot be edited, so deletion is done by destroying keys.

Diagram

sequenceDiagram
  participant App
  participant KMS
  participant DB as Storage
  App->>KMS: GenerateDataKey(kek-id)
  KMS-->>App: DEK plaintext + DEK wrapped
  App->>App: AES-GCM encrypt record with DEK
  App->>DB: store ciphertext + wrapped DEK
  App->>App: discard plaintext DEK
  App->>KMS: Decrypt(wrapped DEK) on read
  KMS-->>App: DEK if authorised (audited)

Worked example

A health records service stores 2 billion documents, average 8 KB, about 16 TB, and serves 20,000 reads per second.

Calling the KMS on every read means 20,000 requests per second, 5 to 20 ms added per read, and about 52 billion billable calls a month. AWS KMS's default shared quota for symmetric operations is 10,000 per second per account and region, 20,000 in some regions and 100,000 in the largest ones, and it counts calls S3 or EBS make on your behalf (AWS KMS quotas), so in most regions this design would be throttled on day one.

Instead, use one DEK per patient and cache unwrapped DEKs in memory for 5 minutes, bounded to 100,000 entries. With a 95 percent cache hit rate, KMS traffic falls to 1,000 calls per second. Local AES-GCM on 8 KB costs a few microseconds.

Storage overhead per record: 12-byte nonce, 16-byte tag, plus a reference to the wrapped DEK (about 180 bytes per patient, stored once). On 16 TB, overhead is well under 1 percent.

A patient deletion request deletes their wrapped DEK. Their documents in 35 days of backups become unreadable immediately, without restoring and rewriting backups.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Disk or volume encryptionZero app changes, covers everythingAnyone with OS or DB access reads plaintextBaseline, always on
Database or storage service encryption with KMSProtects snapshots and backups, audited key useDB users and admins still see plaintextDefault for managed databases and buckets
Application-level field encryptionDatabase and backups hold only ciphertextNo indexing or search on encrypted fields, app complexityHighly sensitive fields, strict insider threat model
Customer-managed keys (BYOK)Customer can revoke accessOperational burden, outage if key disabledEnterprise and regulated customers

Pitfalls in production

Leadership lens

Check yourself

  1. A team enables storage encryption on its database and says customer data is safe from employees. What is wrong?

    Show answer

    The database decrypts transparently for any authorised connection, so admins and anyone with DB credentials still read plaintext. Only application-level encryption with restricted key access stops insiders.

  2. Why is rotating a KEK cheap but rotating DEKs expensive?

    Show answer

    KEK rotation only re-wraps small data keys, or just applies to new wraps. DEK rotation requires decrypting and re-encrypting all the data those keys protect.

  3. How does binding the record id as additional authenticated data help?

    Show answer

    Decryption fails if the ciphertext is moved to another record, so an attacker with write access cannot swap encrypted values between rows.

  4. How does per-user keying make deletion easier?

    Show answer

    Destroying the user's key renders all their ciphertext, including copies in backups, unreadable without touching the backups.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →