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:
| Collection | Encrypted field | What it protects |
|---|---|---|
certificate_private_keys | encrypted_key | The private key PEM that pairs with every issued certificate. |
acme_accounts | encrypted_private_key, eab_hmac_key_encrypted | The account-identity key used to sign ACME requests, and the External Account Binding key where the CA requires one. |
msca_connections | encrypted_credentials | AD service-account username + password. |
dns_credentials | encrypted_config | Every DNS provider's API material. |
module_credentials | encrypted_secret | SSH 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_channels | encrypted_config | SMTP passwords, Slack webhooks, Teams webhooks. |
syslog_tls | encrypted_ca_cert, encrypted_client_cert, encrypted_client_key | The mutual-TLS material the audit forwarder presents to your SIEM. |
project_variables | encrypted_value | Values of variables marked secret. |
discovery_dns_providers | encrypted_config | Credentials for the DNS zones discovery enumerates. |
users | totp_encrypted_secret | 2FA shared secrets. |
license | encrypted_key, encrypted_api_key, encrypted_offline | License-key and activation material. |
approval_requests | params | A 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 encryptedmodule_credentialsentry. 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 inCERTAUTOPILOT_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:
- Before rotation, every envelope has
kek_version: 1. - Operator adds
V2key material and restarts each node so the backend loads both versions.V1is still the keystore's active version — no rotate has happened yet. - Operator runs
certautopilot kek rotate --from-version=1 --to-version=2. The worker reads every record, decrypts it withV1, and re-seals it under a fresh DEK wrapped explicitly with V2 (not with the process's own current — this keeps the outerdoc.kek_versionconsistent with the inner envelopekvtag at all times), writes back atomically per record, and flipsV2active in the keystore viaSwapActive. - Every live node's heartbeat tick (~30 s) detects the SwapActive and hot-reloads
V2as the current KEK — no rolling restart required. New writes from then on seal withV2.V1stays loaded for unwrap of any older envelopes (none, if the rotation ran to completion) untilkek 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 (forpkcs11) 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, enableProtectKernelTunables, 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.envor the K8s Secret) alongside MongoDB.