Encryption
July 20, 2026 · View on GitHub
MongrelDB can encrypt data at rest using AES-256-GCM. When encryption is
enabled, the contents of your sorted-run files (.sr) are unreadable without
the passphrase. This protects sensitive data if someone gains access to the
storage disk.
The Short Version
You can control encryption with a passphrase, a high-entropy raw key, or a HashiCorp Vault Transit key. If the required secret or KMS is unavailable, opening the database fails closed.
// Create an encrypted database
let db = Db::create_encrypted("./mydb", schema, 1, "my-secret-passphrase")?;
// Reopen it later (same passphrase)
let db = Db::open_encrypted("./mydb", "my-secret-passphrase")?;
Everything else - key derivation, per-page encryption, and key wrapping - happens automatically.
Enabling the Feature
Encryption support is always compiled into mongreldb-core.
What Actually Happens Behind the Scenes
When you create an encrypted database, MongrelDB sets up a multi-layer key system. You don't need to understand this to use encryption, but it helps to know why it's secure:
-
Passphrase → KEK. Your passphrase is run through Argon2id (a memory-hard key derivation function that's deliberately slow - about 19 MB of memory and 2 iterations). This produces a 256-bit Key-Encryption Key (KEK). A random 16-byte salt is stored on disk (the salt is not secret - its purpose is to ensure different databases with the same passphrase produce different keys).
-
KEK → DEK. Each sorted-run file gets its own random 256-bit Data Encryption Key (DEK). The DEK is what actually encrypts the page data. The DEK is stored inside the run file, but wrapped (encrypted) by the KEK.
-
KEK → Column keys. For columns marked
ENCRYPTED_INDEXABLE, a per-column key is derived from the KEK. These keys allow the column's values to be transformed into tokens that can be indexed (for equality search) without revealing the plaintext. This uses HMAC for equality tokens and order-preserving encryption (OPE) for range queries.
All keys in memory are held in Zeroizing wrappers - they're overwritten
with zeros when no longer needed.
What Gets Encrypted
| Storage component | Encrypted? | Notes |
|---|---|---|
Sorted-run page data (.sr) | Yes | Each page encrypted independently with AES-256-GCM |
| Sorted-run headers | No | Structural metadata needed to open the file |
WAL segments (_wal/) | Yes (encrypted tables) | Frame-level AES-256-GCM when the table is encrypted |
| Manifest, schema, index files | No | Non-data metadata |
Result cache (_rcache/) | Yes (encrypted tables) | AES-256-GCM encrypted cache files |
Key Files vs Passphrases
MongrelDB supports two ways to provide the encryption key:
Passphrase (human-memorable, slow derivation):
let db = Db::create_encrypted(dir, schema, 1, "my-secret-passphrase")?;
let db = Db::open_encrypted(dir, "my-secret-passphrase")?;
Uses Argon2id to stretch the passphrase into a strong key.
Raw key (machine-generated, fast derivation):
let key = std::fs::read("my.key")?; // 32+ bytes of random data
let db = Db::create_with_key(dir, schema, 1, &key)?;
let db = Db::open_with_key(dir, &key)?;
Skips Argon2id and uses HKDF-SHA256 only. The key must already be
high-entropy (generate one with openssl rand 32 > my.key).
Both paths produce the same KEK; all downstream encryption (sorted runs, WAL, cache) is identical regardless of which method you used.
HashiCorp Vault Transit (externally wrapped random root key):
MONGRELDB_VAULT_TOKEN=... mongreldb-server ./mydb \
--vault-url https://vault.example \
--vault-mount transit \
--vault-key mongreldb-production
Use --vault-ca-cert <path> for an additional private CA and
MONGRELDB_VAULT_NAMESPACE for Vault Enterprise. The token is removed from
the daemon environment before worker threads start. _meta/kms_key.json
contains only the provider identity and Vault ciphertext.
MongrelDB rejects KMS envelope files over 1 MiB and Vault responses over
64 KiB.
Embedded callers use Database::create_with_kms and
Database::open_with_kms. Database::rotate_kms_key rewraps the stable random
database root key under a new Vault Transit key while reads and writes remain
online. The seven-phase journal under _meta/_key_rotation.json resumes after
crashes; Database::retry_kms_key_rotation explicitly retries a durable
Failed rotation.
Note: For encrypted tables, the WAL is also encrypted (frame-level AES-256-GCM). For plaintext tables, the WAL stores rows unencrypted.
Encrypted Indexable Columns
If you want to search encrypted columns (equality or range), mark them
ENCRYPTED_INDEXABLE:
ColumnDef {
id: 2,
name: "ssn".into(),
ty: TypeId::Bytes,
flags: ColumnFlags::empty()
.with(ColumnFlags::ENCRYPTED_INDEXABLE),
}
MongrelDB will:
- Store the encrypted value in the page data (AES-256-GCM)
- Also store a deterministic token (HMAC or OPE) for the index
- The bitmap/HOT indexes use the token, so they work without decrypting
This means Condition::BitmapEq { column_id: 2, value: ... } still works
on encrypted columns - the value is tokenized the same way before lookup.
Performance
Encryption cost depends on the CPU, page size, and workload. Run
cargo bench -p mongreldb-core --bench page_encryption on deployment-class
hardware. Encrypted columns still prune pages through the stats envelope
decrypted at open.
Losing the Passphrase
If you lose the passphrase, the data is unrecoverable. There is no back door, no recovery key, no master override. The KEK cannot be reconstructed without the passphrase, and the DEKs cannot be unwrapped without the KEK.
Store your passphrase securely - a password manager, a secrets service, or wherever you store other critical credentials.
Composing with credential enforcement
Encryption (data-at-rest) and credential enforcement (logical access control) are orthogonal layers. A database can be both encrypted and credentialed:
use mongreldb_core::Database;
// Create an encrypted + credentialed database in one call.
let db = Database::create_encrypted_with_credentials(
"./secure_db",
"my-passphrase", // encryption
"admin", // auth
"s3cret-pw",
)?;
// Reopen requires both the passphrase and the credentials.
let db = Database::open_encrypted_with_credentials(
"./secure_db",
"my-passphrase",
"admin",
"s3cret-pw",
)?;
The passphrase protects the bytes on disk; the credentials protect the operations. Losing either makes the data inaccessible. See Credential Enforcement for details.