Secret delivery option 3: Application-level encryption at rest + runtime decryption #131
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent epic: #129
Overview
Encrypt secret values (AES-256-GCM) in the application layer before storing to PostgreSQL. Decrypt at deploy time in
DeploymentExecutor.buildEnvVars()before passing to Docker containers. This matches exactly what Heroku, Render, and Fly.io do.Current State
Secrets flow as plaintext today:
customEnvVarsvia API → stored as plaintext JSONB inapps.container_configdeployments.resolved_configafterConfigMergerDeploymentExecutor.buildEnvVars()reads and passes to Docker as env varsExisting key material: JWT secret (HMAC-SHA256), Ed25519 key (derived from JWT secret via HMAC). A new, independent encryption key is needed.
Encryption Approach Comparison
Recommendation: Application-level encryption + infrastructure FDE as defense-in-depth. pgcrypto is unsuitable (key appears in SQL logs). TDE only protects disk-level theft.
Java Library Comparison
Cipher.getInstance("AES/GCM/NoPadding"). Manual IV management, no key rotation.Encryptors.stronger()wraps JCA. Limited API.Key Management
Where to Store the Master Key (KEK)
Recommendation:
CAMELEER_SERVER_SECURITY_ENCRYPTIONKEYas env var, backed by K8s Secret (tmpfs). This is exactly what Heroku/Render/Fly.io do. The "circular problem" is a well-understood trade-off — the encryption key protects against a different threat vector (DB dumps, backups, SQLi) than env var exposure.Why NOT Derive from JWT Secret
The Ed25519 key is already derived from JWT secret. While HMAC-SHA256 with distinct context strings is cryptographically sound (NIST SP 800-108), compromising the JWT secret would expose all derived keys simultaneously. Key independence is worth one extra env var.
Envelope Encryption Pattern (DEK/KEK)
Benefits: KEK rotation doesn't require re-encrypting all data (only re-wrap DEKs). Per-app DEKs provide tenant isolation. In SaaS mode, per-tenant KEK is automatic (separate server env per tenant).
Key Rotation Strategy
Store
kek_versiontag alongside each encrypted DEK.Threat Model
What This Defends Against
What This Does NOT Defend Against
docker inspectshows env varsThis is expected and acceptable. Encryption at rest is defense-in-depth for the most common vectors (DB compromise, backup theft). Full server compromise requires different mitigations (network segmentation, IDS).
Compliance Value
What SaaS Platforms Do (State of the Art)
Every major platform encrypts at rest and delivers as env vars. None use sidecar decryptors for standard config vars.
Implementation Plan
Storage Format
Encrypt only the values of
customEnvVars, not the keys. JSONB remains queryable by key name:The
ENC:v1:prefix marks encrypted values. Plaintext values (non-secret config) remain as-is.Steps
CAMELEER_SERVER_SECURITY_ENCRYPTIONKEYenv var (256-bit, Base64, generated at provisioning)server-corePOMSecretEncryptorservice (encrypt/decryptMap<String, String>)PostgresAppRepository.updateContainerConfig()to encryptcustomEnvVarsvaluesPostgresAppRepository.mapRow()to decrypt on readPostgresEnvironmentRepository(environment default config)ENC:prefix), encrypt, write back. Idempotent.DeploymentExecutor.buildEnvVars()before passing to Dockerdeployments.resolved_config(or encrypt there too)Performance Impact
AES-256-GCM throughput is >1 GB/s. Config vars are typically <1 KB.
Searchability Impact
pg_dumpreadable secrets: FixedBackup/Restore
Recommendation
Verdict: ⭐⭐⭐⭐½ (4.5/5)
This should be implemented regardless of which delivery mechanism is chosen. It's a complementary layer that protects the data at rest — the delivery mechanism (env vars, file mount, callback) is a separate concern.
What NOT to Do
customEnvVarsSources