Skip to content

feat: encrypt environment variables at rest with AES-256-GCM#4789

Merged
Siumauricio merged 1 commit into
canaryfrom
feat/encrypt-env-at-rest
Jul 10, 2026
Merged

feat: encrypt environment variables at rest with AES-256-GCM#4789
Siumauricio merged 1 commit into
canaryfrom
feat/encrypt-env-at-rest

Conversation

@Siumauricio

Copy link
Copy Markdown
Contributor

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

  • New encryptedText Drizzle customType encrypts on write (toDriver) and decrypts on read (fromDriver). Services, routers, builders, and prepareEnvironmentVariables are untouched — decryption happens at the ORM boundary.
  • Applied to: application (env, previewEnv, buildArgs, buildSecrets, previewBuildArgs, previewBuildSecrets), compose.env, project.env, environment.env, and env on all 6 database types.
  • Key is derived via HMAC-SHA256 from the existing BETTER_AUTH_SECRET (same pattern as the forward-auth cookie secret) — zero config for existing installs.
  • Optional 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.
  • Stored format is versioned: 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 raw enc: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

  • Restoring a DB backup requires the same BETTER_AUTH_SECRET (or ENCRYPTION_KEY) it was encrypted with — values show as enc:v1: blobs otherwise until re-saved.
  • Rotating BETTER_AUTH_SECRET now affects stored data; setting a dedicated ENCRYPTION_KEY decouples the two.
  • Installs still on the legacy hardcoded auth secret encrypt with a publicly known key — one more reason to migrate to Docker secrets.

@dosubot dosubot Bot added enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files. labels Jul 10, 2026
@Siumauricio Siumauricio merged commit e87a245 into canary Jul 10, 2026
4 checks passed
@Siumauricio Siumauricio deleted the feat/encrypt-env-at-rest branch July 10, 2026 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant