Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/dokploy/__test__/env/encryption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1932,8 +1932,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
<div className="space-y-0.5">
<FormLabel>Docker Cleanup</FormLabel>
<FormDescription>
Trigger the action when Docker cleanup is
performed.
Trigger the action when Docker cleanup is performed.
</FormDescription>
</div>
<FormControl>
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/lib/auth-secret.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
31 changes: 27 additions & 4 deletions packages/server/src/lib/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -21,12 +21,35 @@ const deriveKey = (secret: string) =>

const primaryKey = deriveKey(encryptionSecret ?? betterAuthSecret);

const dedupeKeys = (keys: Buffer[]) => {
const seen = new Set<string>();
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.
Expand Down
Loading