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.
What is encrypted
Section titled “What is encrypted”| 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.
The keys
Section titled “The keys”| 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 |
Key provider strategies
Section titled “Key provider strategies”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 |
Configuration reference
Section titled “Configuration reference”| 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) |
Why windows? (KMS call volume)
Section titled “Why windows? (KMS call volume)”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.
Setting up azure_key_vault
Section titled “Setting up azure_key_vault”- 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.
- Create the KEK as a Key Vault key (not a secret): RSA 2048+ (or
RSA-HSM), operations
wrapKey+unwrapKey. - Grant the application’s managed identity the Key Vault Crypto User role
(or
wrapKey/unwrapKeykey permissions) on the vault. - Set:
Terminal window EP_ENCRYPTION_KEY_PROVIDER=azure_key_vaultEP_ENCRYPTION_KEK_ID=https://<vault>.vault.azure.net/keys/<key-name> - Verify with a document upload round-trip, and confirm
wrapKey/unwrapKeyentries 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.
Rotation
Section titled “Rotation”- KEK (KMS strategies): create a new key version; new writes wrap under it
automatically (when
EP_ENCRYPTION_KEK_IDis 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.