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.
# Print the current key to a secure password managerecho "$ENCRYPTION_MASTER_KEY"Store it 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 — 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).