Database Field Encryption for Sensitive Tokens
Status
Accepted
Context
The Rhesis backend currently stores sensitive credentials as plaintext in the PostgreSQL database, including:
- API keys and authentication tokens in the
endpointtable - Model provider API keys in the
modeltable - OAuth tokens and client secrets
This creates security risks:
- Database dumps or backups could expose sensitive credentials
- Log files or error messages might inadvertently include plaintext secrets
- Unauthorized database access (via SQL injection or compromised credentials) could reveal all tokens
- Compliance and security best practices require encryption at rest for sensitive data
We need a transparent encryption solution that protects data at rest while remaining straightforward to implement and maintain.
Decision
We will implement field-level encryption for sensitive database columns using cryptography.fernet for symmetric encryption with the following approach:
1. Encryption Library: cryptography.fernet
Chosen library: cryptography.fernet
Rationale:
- Part of the widely-used
cryptographypackage in the Python ecosystem - Implements AES-128 in CBC mode with HMAC authentication
- Provides authenticated encryption (prevents tampering and ensures integrity)
- Simple, secure API:
Fernet(key).encrypt()/decrypt() - Returns URL-safe base64-encoded ciphertext suitable for database storage
- Well-documented and actively maintained
- Battle-tested in production environments
Installation:
Basic Usage:
2. Key Management Strategy
Environment Variable: DB_ENCRYPTION_KEY
Key Format:
- 32 URL-safe base64-encoded bytes (Fernet standard format)
- Example:
ZmDfcTF7_60GrrY167zsiPd67pEvs0aGOv2oasOM92s=
Key Generation: Developers can generate keys locally using:
Key Storage by Environment:
Phase 1 (Initial Implementation) - Environment Secrets:
- Local Development:
.envfile (gitignored, never committed) - CI/CD: GitHub Secrets for automated testing
- Staging/Production:
- Kubernetes: Environment variables populated from Kubernetes secrets
- Docker: Environment variable injection at runtime
- Google Cloud Run: Environment variables with GCP Secret Manager reference
Phase 2 (Future Enhancement) - Cloud Secret Manager:
- Direct GCP Secret Manager integration
- Automatic key rotation support
- Centralized audit logging of key access
- Per-environment key isolation with access controls
- Versioned secrets with rollback capability
Security Considerations:
- Keys must never be committed to version control (enforce with
.gitignore) - The same key must be used across all application instances within a single environment
- Critical: Losing the encryption key means permanent loss of access to encrypted data
- Document key backup procedures in deployment documentation
- Store production keys in multiple secure locations
- Consider key escrow for disaster recovery
- Each environment (dev, staging, production) uses a separate encryption key
- Key rotation strategy will be implemented in Phase 2
3. Migration Strategy: In-Place Updates
Approach: Update existing database columns in-place without schema changes
Rationale: Same column names and ORM types; encrypt in place during a controlled rollout instead of maintaining parallel columns.
Migration Flow:
Backward Compatibility Implementation:
The EncryptedString SQLAlchemy TypeDecorator will gracefully handle both encrypted and plaintext values during the migration window:
4. Database Schema: No Changes Required
Existing columns remain as-is:
endpoint.auth_token(Text) → stores encrypted base64 stringendpoint.client_secret(Text) → stores encrypted base64 stringendpoint.last_token(Text) → stores encrypted base64 stringmodel.key(String) → stores encrypted base64 string
Size Considerations: Fernet encryption adds overhead to the stored data:
- Overhead: ~40-60 bytes plus the original length
- Example:
- Original:
"my-api-key"(10 characters) - Encrypted:
"gAAAAABmV8x..."(~120 characters base64-encoded)
- Original:
- Impact: Most token columns are already
Texttype (effectively unlimited), so no schema changes needed
5. Implementation Architecture
SQLAlchemy TypeDecorator:
Encryption/decryption will be transparent to application code using SQLAlchemy’s TypeDecorator:
Usage in Models:
6. Security Model
Threat model (stylized): at-rest ciphertext helps against backup/SQL-dump style exposure; it does not substitute for network access control, least-privilege DB users, or protecting DB_ENCRYPTION_KEY. If both DB and key are lost to the same attacker, they can decrypt.
Phase 1 (implemented): Fernet via EncryptedString, key in env, optional dual-read during migration.
Later: key rotation, Secret Manager/HSM, per-tenant keys, and decrypt audit trails are out of scope for phase 1.
Consequences
Upside: Transparent ORM encryption, no column rename, migration can read mixed plaintext/ciphertext for a window.
Downside: You must operate DB_ENCRYPTION_KEY like production credentials—loss of key is data loss; small per-field CPU cost during rollout.
Every environment needs the key; document how you generate, store, and recover it.
Implementation Plan
Track issues #496–501: TypeDecorator and utils, wire Endpoint/Model (and related CRUD), data backfill script, then integration tests and deployment notes.
Security and operations
Treat DB_ENCRYPTION_KEY like a production secret: gitignored locally, injected from CI/CD or a secret manager in deployed envs, backed up with clear recovery steps, rotated on a schedule, and monitored for decrypt failures or missing env.
Future Work
Possible next steps: dual-key rotation (snippet above), Secret Manager–backed keys, stricter audit of decrypt operations, and extending EncryptedString to other columns if threat model requires it.
References
- Cryptography Library Documentation
- Fernet Specification
- SQLAlchemy TypeDecorator
- OWASP Cryptographic Storage Cheat Sheet
- GCP Secret Manager Best Practices
Related Issues
- Parent: #495 Support for Encrypted Auth Tokens in DB
- Blocks:
- #497 Implement Core Encryption Infrastructure
- #498 Add Encryption to Endpoint Model
- #499 Add Encryption to Model Table
- #500 Data Migration Script
- #501 Integration Testing & Documentation