Choosing a key provider
SetENCRYPTION_PROVIDER to one of the following values and supply the required variables:
If you leave
ENCRYPTION_PROVIDER unset, Ragen auto-detects a provider by checking in order: Scaleway → AWS KMS → local. With none configured, encryption remains off.
Ragen checks the key at startup
Setting the variables is not the same as being able to use the key. Credentials without permission on that specific key, a key id belonging to a different project, or a region that does not hold the key all look like a correct configuration and fail on the first message. So at startup Ragen generates one throwaway data key and unwraps it again, then discards both. Two calls, not one: encrypting and decrypting are separate permissions on both Scaleway Key Manager and an AWS key policy, and a principal that may wrap but not unwrap writes messages nobody can ever read back.
“Refused” means a permanent answer: any
4xx other than 408 or 429, an AWS refusal, a malformed response, or a data key that comes back as different bytes than were wrapped.
When a deployment is refused, check the three things that produce an identical permission error: whether these credentials may use this key id in this region. Any of the three can be wrong on its own, and the error text is the same in each case.
How envelope encryption works
1
Thread key generation
When a new conversation thread is created, Ragen requests a fresh data encryption key (DEK) from your configured key provider. The provider returns both the plaintext DEK and a wrapped (encrypted) copy of that DEK.
2
Message encryption
All message content written to that thread is encrypted using the plaintext DEK with AES-256-GCM. The plaintext DEK is held only in memory for the duration of the request and never written to disk.
3
Wrapped key storage
The wrapped DEK is stored alongside the thread record in Postgres. To decrypt a thread later, Ragen unwraps the DEK using the key provider, then uses it to decrypt the message content.
4
Thread titles stay plaintext
Thread titles are stored in plaintext deliberately so that title search continues to work. Content search over encrypted threads is title-only — a deliberate trade-off.
In-transit encryption
TLS secures all connections to the application, terminated by your own ingress. Internal service-to-service calls are additionally authenticated with HMAC-signed short-lived tokens, so no internal component can be impersonated from within the network.Impact on features
Enabling encryption changes the behaviour of two features:Langfuse traces
With encryption on,
input and output fields are omitted from Langfuse traces. Only tags, session ID, and model are recorded — message content is never sent to the tracing backend.Content search
Search across threads matches on thread titles only. Message content is not searched whether or not it is encrypted.
Consciously opting out
If you have evaluated the trade-offs and need to run a deployed environment without encryption — for example, during a migration — setALLOW_UNENCRYPTED=1 and configure no key provider. This is a conscious, logged opt-out:
Verifying encryption is active
After configuring a key provider and restarting your deployment, confirm encryption is working by checking a newly created thread in Postgres:encrypted_dek is populated are encrypted. If encrypted_dek is null on new threads, the key provider configuration has not reached the process — recheck your environment variables and restart.
A key that is configured but refused does not reach this check: the startup probe stops the deployment first. Reaching a running application with null
encrypted_dek on new threads therefore points at variables that never arrived, not at a key the provider rejected.