Database Field Encryption
Sensitive credentials (endpoint auth tokens and client secrets, model provider API keys, user tokens, tool credentials) are encrypted at rest in PostgreSQL using field-level encryption. This protects against exposure through database dumps, backups, or direct read access; it is not a substitute for network access control, least-privilege DB users, or protecting the key.
Library: cryptography.fernet
Fernet provides authenticated symmetric encryption (AES-128-CBC with HMAC) and returns URL-safe base64 ciphertext suitable for storage in text columns. It is already a backend dependency.
The application encrypts and decrypts through helpers in
rhesis.backend.app.utils.encryption (encrypt, decrypt, is_encrypted), which use a single
cached Fernet instance built from the configured key.
Key management
The key is read from the DB_ENCRYPTION_KEY environment variable — 32 URL-safe base64-encoded
bytes (Fernet’s standard format), e.g. ZmDfcTF7_60GrrY167zsiPd67pEvs0aGOv2oasOM92s=. It is a
required setting with no default: the backend refuses to start if it is missing or not a valid
Fernet key.
Generate a key with:
Storage by environment:
- Local development:
.env(gitignored, never committed) - CI: GitHub Secrets
- Deployed: Kubernetes secrets / GCP Secret Manager, injected as an environment variable
Operational rules:
- Use the same key across all instances in an environment; use a separate key per environment.
- Losing the key means permanent loss of access to the encrypted data. Back up production keys in more than one secure location.
- Rotation is not automatic: the cached cipher is built once per process, so changing the key requires re-encrypting all stored values and restarting the backend.
Implementation
The EncryptedString SQLAlchemy TypeDecorator makes encryption transparent to application code —
it encrypts on write and decrypts on read. Its backing type is String (unlimited by default, or
String(length) when a length is passed).
Use it as a column type:
Reads fail loudly
decrypt does not fall back to the raw stored value. A value that cannot be decrypted — because it
is still plaintext, or was encrypted with a different DB_ENCRYPTION_KEY — raises DecryptionError
rather than leaking ciphertext or plaintext to callers. The one-time data migration must therefore
run before any encrypted column is read.
Non-deterministic ciphertext and lookups
Fernet output is non-deterministic, so encrypted columns cannot be queried directly. For values
that need indexed lookup (user tokens), a SHA-256 hash of the plaintext (hash_token) is stored
alongside the encrypted value and used as the lookup key.
Encrypted columns
Encryption is applied in place; column types and names are unchanged. Columns currently using
EncryptedString:
endpoint.auth_token,endpoint.client_secret,endpoint.last_tokenmodel.keytoken.tokentool.credentials
Fernet adds roughly 40-60 bytes plus the base64-expanded plaintext, well within the existing text columns.
Migration
Existing plaintext values are encrypted by Alembic revision da9164715ec2:
It processes endpoint, model, and token in batches and is idempotent — values already in
Fernet format (prefix gAAAAA) are skipped, so it is safe to re-run. The downgrade reverses it,
decrypting values back to plaintext for rollback only.
For diagnosing problems, see Encryption Troubleshooting and the broader Security notes.