diff --git a/apps/dokploy/__test__/env/encryption.test.ts b/apps/dokploy/__test__/env/encryption.test.ts index 6e4f8f74d2..b30e5954ba 100644 --- a/apps/dokploy/__test__/env/encryption.test.ts +++ b/apps/dokploy/__test__/env/encryption.test.ts @@ -49,6 +49,37 @@ describe("encryptValue / decryptValue", () => { }); }); +describe("migrating off the hardcoded legacy BETTER_AUTH_SECRET", () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + }); + + const loadWithAuthSecret = async (secret: string) => { + vi.stubEnv("BETTER_AUTH_SECRET", secret); + vi.resetModules(); + return await import("@dokploy/server/lib/encryption"); + }; + + it("still decrypts values written under the legacy secret", async () => { + // Encrypted while the install was still on the hardcoded default + const legacyEncrypted = encryptValue("KEY=legacy-value"); + + const migrated = await loadWithAuthSecret("a-real-auth-secret"); + expect(migrated.decryptValue(legacyEncrypted)).toBe("KEY=legacy-value"); + }); + + it("encrypts new values with the new secret, not the legacy one", async () => { + const migrated = await loadWithAuthSecret("a-real-auth-secret"); + const encrypted = migrated.encryptValue("KEY=post-migration"); + + // The legacy-derived key alone cannot read it, proving the write used + // the migrated secret + expect(() => decryptValue(encrypted)).toThrow(/BETTER_AUTH_SECRET/); + expect(migrated.decryptValue(encrypted)).toBe("KEY=post-migration"); + }); +}); + describe("dedicated ENCRYPTION_KEY", () => { afterEach(() => { vi.unstubAllEnvs(); diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index c2e249c2df..ddcef338e2 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -1932,8 +1932,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
Docker Cleanup - Trigger the action when Docker cleanup is - performed. + Trigger the action when Docker cleanup is performed.
diff --git a/packages/server/src/lib/auth-secret.ts b/packages/server/src/lib/auth-secret.ts index 8e709758fd..a45da74316 100644 --- a/packages/server/src/lib/auth-secret.ts +++ b/packages/server/src/lib/auth-secret.ts @@ -1,6 +1,6 @@ import { readSecret } from "../db/constants"; -const HARDCODED_LEGACY_SECRET = "better-auth-secret-123456789"; +export const HARDCODED_LEGACY_SECRET = "better-auth-secret-123456789"; const { BETTER_AUTH_SECRET, BETTER_AUTH_SECRET_FILE } = process.env; diff --git a/packages/server/src/lib/encryption.ts b/packages/server/src/lib/encryption.ts index 69a0805953..f1bbadbd60 100644 --- a/packages/server/src/lib/encryption.ts +++ b/packages/server/src/lib/encryption.ts @@ -7,7 +7,7 @@ import { import { readFileSync } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; -import { betterAuthSecret } from "./auth-secret"; +import { betterAuthSecret, HARDCODED_LEGACY_SECRET } from "./auth-secret"; import { encryptionSecret } from "./encryption-secret"; export const ENCRYPTION_KEY_BACKUP_FILE = "encryption.key"; @@ -21,12 +21,35 @@ const deriveKey = (secret: string) => const primaryKey = deriveKey(encryptionSecret ?? betterAuthSecret); +const dedupeKeys = (keys: Buffer[]) => { + const seen = new Set(); + return keys.filter((key) => { + const hex = key.toString("hex"); + if (seen.has(hex)) { + return false; + } + seen.add(hex); + return true; + }); +}; + // Installs that adopt a dedicated ENCRYPTION_KEY still hold values encrypted // with the auth-secret-derived key; keep it as a decrypt fallback so they // lazily re-encrypt on the next write instead of becoming unreadable. -const decryptionKeys = encryptionSecret - ? [primaryKey, deriveKey(betterAuthSecret)] - : [primaryKey]; +// +// The same applies to installs migrating off the deprecated hardcoded auth +// secret, which the startup banner asks every affected user to do: their +// values are encrypted with the legacy-derived key. HARDCODED_LEGACY_SECRET +// is a published constant, so keeping it as a decrypt-only fallback discloses +// nothing that was not already derivable from the source, while stopping the +// migration from orphaning existing values. Encryption always uses +// primaryKey, so values written after the migration are protected by the +// real secret. +const decryptionKeys = dedupeKeys([ + primaryKey, + ...(encryptionSecret ? [deriveKey(betterAuthSecret)] : []), + deriveKey(HARDCODED_LEGACY_SECRET), +]); // Derived keys only — never the raw secrets. A leaked key can decrypt // stored values, but the raw BETTER_AUTH_SECRET could also forge sessions.