Encryption Setup
Engram encrypts all sensitive data at rest with AES-GCM. The model is worth understanding before you operate a self-host instance for real. (For the deeper “what’s encrypted, AAD binding, wire format” reference, see Concepts → Encryption Model.)
The model in one minute
Section titled “The model in one minute”Engram uses a two-key hierarchy:
master key (env) → wraps → per-user DEK (db) → encrypts → user data- Master key lives in
ENCRYPTION_MASTER_KEY(env var, never on disk in plaintext). Used only to wrap/unwrap DEKs in memory. - Per-user DEK is a random AES-256 key generated at user creation. It encrypts that user’s notes, attachments, search payloads, and metadata. Stored in the database wrapped by the master key.
The split lets master-key rotation re-wrap every DEK without re-encrypting user data, and lets a single user’s DEK rotate without touching anyone else’s.
For an operator, two implications matter most:
- Losing the master key = losing access to all user data, since no DEK can be unwrapped
- Database backups alone are not enough. You also need the master key, stored separately
Back up the master key
Section titled “Back up the master key”Do this before the stack ever sees production data.
The key lives in your .env file. Look it up there (avoid echoing it to your terminal, since most shells log to history):
grep ^ENCRYPTION_MASTER_KEY= .envStore the value in:
- A password manager (1Password, Bitwarden, vault)
- An offline copy (USB drive in a safe)
- Optionally, a copy with your trusted custodian
You will need this exact string to restore from any backup. Treat it like a private key, because that’s what it is.
Rotating the master key
Section titled “Rotating the master key”The backend ships mix engram.rotate_master_key --target-version <N>,
which re-wraps every user’s DEK with a new master key. Procedure:
- Generate the new key:
openssl rand -base64 32 - Set the new key as
ENCRYPTION_MASTER_KEYand move the previous value toENCRYPTION_MASTER_KEY_PREVIOUS(rescue lane during rotation) - Bump
ENCRYPTION_MASTER_KEY_VERSION(e.g.1→2) - Run the rotation task. It iterates users, re-wraps their DEKs
under the new version, and bumps
dek_versionper row - When complete, remove
ENCRYPTION_MASTER_KEY_PREVIOUS - Restart the app, and the boot canary re-engages against the new key
The full runbook (per-user rotation, AAD rebind, half-state
recovery) lives in
backend/docs/context/encryption-operations.md.
If the app boots with the wrong master key, a boot canary check fails loudly with a clear error. You can’t accidentally run with a broken key. The app refuses to start.
Rotating a single user’s DEK
Section titled “Rotating a single user’s DEK”A per-user rotation cycles that user’s DEK only, useful after a suspected token compromise. Run:
mix engram.rotate_user_dek --user-id <ID>It re-encrypts that user’s notes, attachments, and Qdrant payloads under a fresh DEK wrapped by the same master key. The task acquires an advisory lock so concurrent writes don’t observe a mixed-DEK state.
What the database actually looks like
Section titled “What the database actually looks like”In Postgres, encrypted columns are bytea. The wire format is
versioned: <version_byte, nonce, ciphertext, auth_tag>. AAD
(Additional Authenticated Data) binds each ciphertext to the row
that owns it, so a copy-paste attack across rows is rejected at
decrypt time.
For deeper detail see
backend/docs/context/encryption-operations.md
in the backend repo (the runbook this section is derived from).