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.