Skip to main content

Envelope encryption deep-dive

Every sensitive field in CertAutoPilot's database is protected by a two-level key hierarchy: a per-field Data Encryption Key (DEK) encrypts the payload; a single Key Encryption Key (KEK) wraps the DEK. This page is the cryptographic anatomy — what's in each envelope, how encryption and decryption flow, why the design looks this way.

For the user-facing overview and the list of what's encrypted, see Encryption → Overview. For rotation mechanics, see KEK rotation.

Envelope shape

Every encrypted field lands in MongoDB as a single base64-encoded string — not a sub-document, and not BinData. Decoded, it is a small JSON object:

{
"edek": "<base64>", // the DEK, sealed by the KEK
"ct": "<base64>", // AES-256-GCM output: nonce ‖ ciphertext ‖ tag
"kv": 2, // which KEK version sealed this envelope
"p": "env", // "env" or "pkcs11"
"ab": true // present only when the value is field-bound
}

There is no separate nonce member: the 12-byte nonce is prefixed to the ciphertext and the authentication tag is appended, following Go's cipher.AEAD.Seal convention. The binding context behind ab is never stored — it is reconstructed at read time from where the value lives, because storing it would let anyone moving the ciphertext carry the matching context along with it.

Field names matter if you are writing a mongodump inspection script or a disaster-recovery verification tool: these five are what is actually on disk.

Write path

  1. Generate 32 random bytes → DEK.
  2. Generate a 12-byte nonce — always, on every path.
  3. ciphertext = AES-256-GCM(payload, DEK, nonce).
  4. Ask the active KEK provider to wrap the DEK:
    • env: AES-256-GCM of the DEK under the KEK bytes (with its own internal nonce).
    • pkcs11: call CKM_AES_GCM on the HSM handle for version N.
  5. Write the envelope doc.
  6. Discard the DEK from memory.

The DEK never persists. The KEK is in memory (env) or inaccessible (pkcs11). A mongodump yields only wrapped_dek + ciphertext — decryption requires both the KEK and the ability to run AES-GCM, which is what the running backend process has.

Read path

  1. Read the envelope doc.
  2. Check provider — reject if it disagrees with the configured provider (defence against install-lock bypass).
  3. Ask the provider to unwrap the DEK using kek_version. If the version isn't loaded, abort with a clear error.
  4. payload = AES-256-GCM⁻¹(ciphertext, DEK, nonce).
  5. On AEAD tag mismatch: abort. The envelope was tampered with.
  6. Return payload; discard DEK.

Why a per-field DEK

  • Blast radius. A leak of one envelope is one field. Since each field has its own DEK, the adversary doesn't get a skeleton key.
  • Unlimited AES-GCM message count. AES-GCM has a nonce-space limit of 2⁹⁶ per key. Using a fresh DEK per field trivially stays far below the birthday bound.
  • Rotation locality. Rotation is per-record and incremental: the worker decrypts one field at a time, re-seals it under a fresh DEK wrapped by the new KEK, and writes that record back on its own. Records already on the target version are skipped, a rotation can be cancelled and resumed at a batch boundary, and nothing else in the database is touched. Cost scales with the size of the secrets as well as their number, so size a maintenance window against your actual secret volume rather than assuming a fixed per-record cost.

Nonce size

CertAutoPilot always uses the 12-byte nonce that AES-GCM prescribes, for both the payload and the DEK wrap, and pins that length explicitly on the PKCS#11 path. If an HSM driver reports a different GCM IV length at startup, the backend refuses to start rather than sealing data under an unexpected parameter. There is no adaptive fallback.

Provider tag safety

Tagging each envelope with the provider that sealed it is a defensive check: the backend refuses to unwrap a provider: env envelope when the active provider is pkcs11, and vice versa. This prevents silent data corruption if someone mutates the kek_install record manually or tries to mix two installs' data.

Version tag and rotation

kek_version lets one backend instance unwrap envelopes sealed by any previously-loaded version. Multiple versions can be loaded simultaneously — that's what makes the rotation workflow non-disruptive:

  1. Before rotation: every envelope has kek_version: 1.
  2. Add V2. Both V1 and V2 are loaded. New writes seal with V2. Old reads unwrap with V1.
  3. Run kek rotate. The worker reads each record, decrypts it with V1, re-encrypts it under a fresh DEK wrapped by V2, and writes it back. kek_version flips to 2.
  4. After rotation: every envelope is kek_version: 2. V1 can be retired.

The algorithm is the same for env and pkcs11. See KEK rotation.

Threat model

In scope

  • MongoDB compromise / backup leak. Envelope encryption makes a stolen DB useless without the KEK.
  • Read-replica exposure. Same story — encrypted payload is encrypted payload.
  • Operator misbehaviour at the DB tier. A DBA with find() access still needs the KEK to learn anything sensitive.
  • Cloud-provider data exfil. Disk snapshot of the MongoDB volume doesn't expose cleartext.

Out of scope

  • Running-process memory. The backend must hold the KEK (env) or an HSM handle (pkcs11) to operate. A memory scraper inside the process can extract either. Mitigate with LimitCORE=0, MemoryDenyWriteExecute=true, host-level access controls.
  • Catastrophic secret loss. Lose the KEK — standalone's secrets.env, K8s's Secret, or the HSM key material — and encrypted data is unrecoverable. Back up both the DB AND the secret store.
  • Side channels on the HSM. CertAutoPilot uses the HSM's standard API; any side-channel vulnerability in the vendor's implementation is out of our hands.

Compared to MongoDB client-side field-level encryption (CSFLE)

MongoDB's CSFLE does something conceptually similar but requires the enterprise DB + a specific driver setup. Our envelope implementation runs at the Go code layer, works against any MongoDB 6.0+ cluster, and ships with HSM-backed key custody via PKCS#11. The two can coexist if you want belt-and-braces.

What is not envelope-encrypted

  • User passwords — bcrypt hashed; irreversible by design.
  • Refresh tokens — SHA-256 hashed; reuse detection needs a fixed digest for comparison.
  • Certificate PEMs (public parts) — safe to expose.

Full list and the collection-level map: Encryption → Overview.

See also