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:

  1. 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).

  2. 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.

  3. 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 componentEncrypted?Notes
Sorted-run page data (.sr)YesEach page encrypted independently with AES-256-GCM
Sorted-run headersNoStructural 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 filesNoNon-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.