Skip to content

Encryption & Key Management

Enprivacy 3.0 encrypts sensitive data at the application level — encrypted values reach the database as ciphertext, so database-level encryption (TDE, encrypted volumes) is a second layer underneath, not the only protection. This guide covers the layers, the keys, the selectable key-provider strategies, and their configuration. Everything is configured through environment variables.

Data Mechanism
Document content, database samples, connection configs, credentials, API keys, DPO contact details Application-level encryption (Active Record Encryption, AES-256-GCM)
Sign-in identities — emails, phone numbers, OIDC subjects, session tokens Application-level encryption, plus a one-way digest index for lookups (see below)
Everything else at rest Your platform’s storage encryption (e.g. Azure Storage Service Encryption, TDE)

Searchable encrypted fields use a keyed one-way digest (HMAC-SHA256) stored in an adjacent *_digest column: lookups match on the digest, so equality is the only thing the index reveals — a database leak cannot be reversed into the values, and unlike deterministic encryption there is no key that decrypts the index. The digest key (EP_ENCRYPTION_INDEX_KEY) must stay stable for the life of the data.

Key Supplied via Rotatable?
SECRET_KEY_BASE env / secret store Yes — invalidates active sessions
EP_ENCRYPTION_PRIMARY_KEY env / secret store Yes — comma-delimited key list; append the new key last (see Rotation below)
EP_ENCRYPTION_KEY_DERIVATION_SALT env / secret store Never — all ciphertext depends on it (the app refuses to boot if it contains a comma)
EP_ENCRYPTION_INDEX_KEY env / secret store Never without re-indexing — digests are computed under it
KEK (EP_ENCRYPTION_KEK_ID) lives in your KMS; never leaves it Yes — new versions apply to new writes; each record names the version that wrapped it, so old data needs no rewrite

Select with EP_ENCRYPTION_KEY_PROVIDER. The same environment surface applies across strategies, so switching (or adopting a future backend) is configuration, not migration — records name the key that protects them, and each strategy can still read data written by the previous one.

Strategy What it does When to use
local (default) Rails’ derived-key provider: content keys derived from the primary key Simple deployments; no external dependency
envelope Rails’ built-in envelope: a fresh data key per encryption operation, wrapped locally under the primary key Per-record keys without a KMS
azure_key_vault Cloud envelope encryption: content encrypted locally with per-window AES-256 data keys (DEKs); the key-encryption key (KEK) is an HSM-backed Key Vault key that never leaves the vault — Key Vault only ever wraps/unwraps DEKs Azure deployments needing “the master key never leaves an HSM”
aws_kms, gcp_kms Reserved — same shape: EP_ENCRYPTION_KEK_ID carries the key ARN / resource name Future
Variable Default Applies to Purpose
EP_ENCRYPTION_KEY_PROVIDER local all Strategy selector (above)
EP_ENCRYPTION_KEK_ID — KMS strategies The key identifier, in the backend’s native form. Azure: https://<vault>.vault.azure.net/keys/<name>[/<version>] (no version = current)
EP_ENCRYPTION_WRAP_ALGORITHM RSA-OAEP-256 KMS strategies Wrap/unwrap algorithm
EP_ENCRYPTION_WINDOW_SECONDS 60 KMS strategies Data-key window: one KMS wrap call per window per process; every record written inside the window shares the DEK. Longer = fewer KMS calls, more records per DEK
EP_ENCRYPTION_UNWRAP_CACHE_TTL_SECONDS 600 KMS strategies How long unwrapped DEKs stay in the per-process read cache
EP_ENCRYPTION_UNWRAP_CACHE_MAX_ENTRIES 1024 KMS strategies Read-cache size bound (LRU)
EP_ENCRYPTION_IDENTITY_CLIENT_ID — azure_key_vault Which managed identity acquires vault tokens, when the host carries more than one
EP_ENCRYPTION_INDEX_KEY — all HMAC key for the *_digest search indexes (required in production)

Envelope encryption without batching would call the KMS once per record — a 10,000-row detection job would make 10,000 wrapKey calls and hit vault throttling immediately. The windowed design mints one data key per window per process: the same job costs a handful of KMS calls, and because whole windows share a DEK, reading those records back needs only one unwrapKey per window (then cache hits). Tune EP_ENCRYPTION_WINDOW_SECONDS against your vault tier’s measured limits — longer windows mean fewer calls but more records protected by a single DEK.

Plaintext data keys exist only in process memory, bounded by the window (on write) and the cache TTL (on read); they are never persisted or shared between replicas.

  1. Create (or reuse) a Key Vault — Premium tier or Managed HSM so the KEK is HSM-backed — with public network access disabled and a private endpoint, consistent with the reference architecture.
  2. Create the KEK as a Key Vault key (not a secret): RSA 2048+ (or RSA-HSM), operations wrapKey + unwrapKey.
  3. Grant the application’s managed identity the Key Vault Crypto User role (or wrapKey/unwrapKey key permissions) on the vault.
  4. Set:
    Terminal window
    EP_ENCRYPTION_KEY_PROVIDER=azure_key_vault
    EP_ENCRYPTION_KEK_ID=https://<vault>.vault.azure.net/keys/<key-name>
  5. Verify with a document upload round-trip, and confirm wrapKey/unwrapKey entries appear in the vault’s audit log.

Availability note: with a KMS strategy, the KMS is on the decryption path. The in-process caches mask brief blips (and the client retries), but a sustained KMS outage means encrypted reads fail — factor the KMS SLA into your availability planning.

  • KEK (KMS strategies): create a new key version; new writes wrap under it automatically (when EP_ENCRYPTION_KEK_ID is versionless). Every record stores the exact version that wrapped its data key, so existing data keeps decrypting with no rewrite. Old versions must remain enabled until no data references them.
  • Primary key: rotate by appending the new key to the comma-delimited list — EP_ENCRYPTION_PRIMARY_KEY=oldkey,newkey. The last key becomes the active encryption key; every listed key keeps decrypting, so existing ciphertext needs no rewrite. Never remove a key that stored data may still be encrypted under. Each key must be ≥ 32 characters (validated at boot).
  • Salt / index key: unrotatable by design — the salt underpins every derived key (past and present), and rotating the index key orphans every search digest until re-indexed. The app refuses to boot if the salt contains a comma (a rotation attempt on the wrong variable).
  • SECRET_KEY_BASE: rotate freely; users re-authenticate.