Skip to content

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

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

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):

Terminal window
grep ^ENCRYPTION_MASTER_KEY= .env

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

The backend ships mix engram.rotate_master_key --target-version <N>, which re-wraps every user’s DEK with a new master key. Procedure:

  1. Generate the new key: openssl rand -base64 32
  2. Set the new key as ENCRYPTION_MASTER_KEY and move the previous value to ENCRYPTION_MASTER_KEY_PREVIOUS (rescue lane during rotation)
  3. Bump ENCRYPTION_MASTER_KEY_VERSION (e.g. 12)
  4. Run the rotation task. It iterates users, re-wraps their DEKs under the new version, and bumps dek_version per row
  5. When complete, remove ENCRYPTION_MASTER_KEY_PREVIOUS
  6. 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.

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.

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