Skip to main content

Envelope encryption

Every sensitive field that lands in MongoDB is sealed with AES-256-GCM. A fresh random data encryption key (DEK) is generated per field and wrapped by the active key encryption key (KEK). This page explains the model, what is encrypted, and what appears on disk.

The envelope

Every encrypted field is stored as a single base64-encoded string, not as a sub-document. 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.

The DEK is never reused across fields — each record gets its own. The KEK, by contrast, is long-lived and versioned so it can be rotated without re-encrypting every record at once (see KEK rotation).

What is encrypted

Every field you would rather not see in a mongodump is sealed:

CollectionEncrypted fieldWhat it protects
certificate_private_keysencrypted_keyThe private key PEM that pairs with every issued certificate.
acme_accountsencrypted_private_key, eab_hmac_key_encryptedThe account-identity key used to sign ACME requests, and the External Account Binding key where the CA requires one.
msca_connectionsencrypted_credentialsAD service-account username + password.
dns_credentialsencrypted_configEvery DNS provider's API material.
module_credentialsencrypted_secretSSH keys, kubeconfigs, F5/NetScaler/IIS passwords, Vault tokens, Huawei AK/SK. Empty for a credential that reads from an external secret store — there is no value here to encrypt.
notification_channelsencrypted_configSMTP passwords, Slack webhooks, Teams webhooks.
syslog_tlsencrypted_ca_cert, encrypted_client_cert, encrypted_client_keyThe mutual-TLS material the audit forwarder presents to your SIEM.
project_variablesencrypted_valueValues of variables marked secret.
discovery_dns_providersencrypted_configCredentials for the DNS zones discovery enumerates.
userstotp_encrypted_secret2FA shared secrets.
licenseencrypted_key, encrypted_api_key, encrypted_offlineLicense-key and activation material.
approval_requestsparamsA secret project-variable value, while a configuration change waits for approval. Transient — it goes when the request is executed, rejected, or expires after 7 days.

Every one of these carries its own key version and is covered by key rotation and by the checks that guard retiring a key — with one exception: the value nested inside approval_requests.params is not rotated, because it lives inside a JSON field rather than in a column of its own. Retiring a key is still safe: kek remove refuses while any such value is outstanding, and it clears itself when the request completes or expires.

PEM certificate bodies (public) and the ACME registration URL are not encrypted — they are safe to expose.

What is not envelope-encrypted

  • User passwords — bcrypt hashed (not encrypted; irreversible by design).
  • Refresh tokens — SHA-256 hashed (reuse detection requires a fixed digest).
  • External secret-store connection settings (secret_stores) — the address, CA certificate and store-specific options are stored and returned in plaintext by design; any project viewer can read them. A store's own credential is never kept here, only a reference to an encrypted module_credentials entry. Never put secret material in a store's configuration.
  • API keys — SHA-256 hashed with a server-side pepper; only the last four characters are kept, for display.
  • Certificate bodies and chains — public data, stored as-is (see above).
  • MongoDB-at-rest — orthogonal concern. If your policy requires at-rest volume encryption, configure it on the MongoDB side (LUKS, cloud-provider CMK, WiredTiger encryption). Envelope encryption protects against a logical leak (mongodump, read-replica snapshot); volume encryption protects against physical theft. Run both.

KEK providers

The wrap/unwrap of the DEK is delegated to a provider. CertAutoPilot ships with two:

  • env — Phase 1 default. Raw KEK bytes live in CERTAUTOPILOT_ENCRYPTION_ENV_KEK_V{N} environment variables. Simplest to operate; the backend process must be trusted with the key material.
  • pkcs11 — Phase 2. The KEK lives inside a PKCS#11 HSM. CertAutoPilot only holds opaque handles and the wrapped DEK blobs; the key material never leaves the HSM.

The choice is install-locked in MongoDB (kek_install singleton). Swapping between env and pkcs11 on an already-provisioned database is rejected — see provider migration.

The kek_version field

Every envelope carries the version of the KEK that sealed it. This is what makes rotation incremental:

  1. Before rotation, every envelope has kek_version: 1.
  2. Operator adds V2 key material and restarts each node so the backend loads both versions. V1 is still the keystore's active version — no rotate has happened yet.
  3. Operator runs certautopilot kek rotate --from-version=1 --to-version=2. The worker reads every record, decrypts it with V1, and re-seals it under a fresh DEK wrapped explicitly with V2 (not with the process's own current — this keeps the outer doc.kek_version consistent with the inner envelope kv tag at all times), writes back atomically per record, and flips V2 active in the keystore via SwapActive.
  4. Every live node's heartbeat tick (~30 s) detects the SwapActive and hot-reloads V2 as the current KEK — no rolling restart required. New writes from then on seal with V2. V1 stays loaded for unwrap of any older envelopes (none, if the rotation ran to completion) until kek remove + env-var cleanup clears it.

See KEK rotation for the full runbook.

The provider field

Tagging each envelope with the provider that sealed it is a safety net: the backend refuses to unwrap a provider: env envelope when the active provider is pkcs11, and vice versa, preventing silent data corruption if someone edits the kek_install record by hand.

Guarantees and limits

  • Confidentiality + integrity via AES-256-GCM. Tampering with the ciphertext yields an authenticated-decryption failure.
  • Per-field DEK. A leak of one envelope does not weaken any other.
  • In-memory exposure: the backend holds the KEK (for env) or the HSM handle (for pkcs11) in memory; a running-process memory dump reveals it. This is inherent to online cryptosystems — use OS-level memory protection (disable core dumps for the unit, enable ProtectKernelTunables, and so on).
  • Key custody is on you. CertAutoPilot does not have a "recover my KEK" flow. Lose the KEK → envelopes become unreadable. Back up secrets (standalone secrets.env or the K8s Secret) alongside MongoDB.

See also