feat: encrypt environment variables at rest with AES-256-GCM#4789
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Environment variables (and build args/secrets) are currently stored in plaintext in the Dokploy database. Anyone with access to a DB dump, backup, or the Postgres instance can read every secret of every service. This PR encrypts them at rest with AES-256-GCM.
Related to #2817 — same underlying problem (plaintext env variables as a security risk), addressed at the storage layer instead of UI-only masking. A previous attempt, #2819 (closed), encrypted values only for UI display while explicitly keeping plaintext in the DB; this PR takes the opposite approach: the DB stores ciphertext, and the UI keeps working with plaintext transparently.
How it works
encryptedTextDrizzlecustomTypeencrypts on write (toDriver) and decrypts on read (fromDriver). Services, routers, builders, andprepareEnvironmentVariablesare untouched — decryption happens at the ORM boundary.application(env, previewEnv, buildArgs, buildSecrets, previewBuildArgs, previewBuildSecrets),compose.env,project.env,environment.env, andenvon all 6 database types.BETTER_AUTH_SECRET(same pattern as the forward-auth cookie secret) — zero config for existing installs.ENCRYPTION_KEY/ENCRYPTION_KEY_FILE(Docker secret convention) takes priority as the primary key. Decryption tries the primary key first and falls back to the auth-secret-derived key, so adopting a dedicated key never orphans existing data.enc:v1:<base64(iv | authTag | ciphertext)>, leaving room for future key rotation (v2) without painful migrations.Migration
Lazy — no DB migration needed (columns stay
text, drizzle-kit reports no schema changes). Legacy plaintext values pass through on read and are encrypted the next time they are saved.Reads are fail-open: if a stored value can't be decrypted (e.g. a backup restored under a different
BETTER_AUTH_SECRET), the rawenc:v1:string is returned and an error is logged, instead of breaking every query that touches the row. Re-saving the value recovers it.Notes
BETTER_AUTH_SECRET(orENCRYPTION_KEY) it was encrypted with — values show asenc:v1:blobs otherwise until re-saved.BETTER_AUTH_SECRETnow affects stored data; setting a dedicatedENCRYPTION_KEYdecouples the two.