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.

Terminal window
# Print the current key to a secure password manager
echo "$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.

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