Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(),
})
.required(),
branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const GitProviderSchema = z.object({
repositoryURL: z.string().min(1, {
message: "Repository URL is required",
}),
branch: z.string().min(1, "Branch required").regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z
.string()
.min(1, "Branch required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ const GithubProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ const GitlabProviderSchema = z.object({
id: z.number().nullable(),
})
.required(),
branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(),
})
.required(),
branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const GitProviderSchema = z.object({
repositoryURL: z.string().min(1, {
message: "Repository URL is required",
}),
branch: z.string().min(1, "Branch required").regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z
.string()
.min(1, "Branch required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ const GitlabProviderSchema = z.object({
gitlabPathNamespace: z.string().min(1),
})
.required(),
branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
Expand Down
1 change: 1 addition & 0 deletions apps/dokploy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts",
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
"reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs",
"migrate-auth-secret": "tsx -r dotenv/config scripts/migrate-auth-secret.ts",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
Expand Down
97 changes: 97 additions & 0 deletions apps/dokploy/scripts/migrate-auth-secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Use this command to automatically migrate the auth secret: curl -sSL https://dokploy.com/security/0.29.3.sh | bash
* Migration script: re-encrypt 2FA secrets after rotating BETTER_AUTH_SECRET.
*
* Usage:
* OLD_SECRET=<old_secret> NEW_SECRET=<new_secret> npx tsx apps/dokploy/scripts/migrate-auth-secret.ts
*
* Both OLD_SECRET and NEW_SECRET are required.
* Run this BEFORE restarting Dokploy with the new secret.
*/
import { db } from "@dokploy/server/db";
import { twoFactor } from "@dokploy/server/db/schema";
import { symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
import { eq } from "drizzle-orm";

const OLD_SECRET = process.env.OLD_SECRET as string;
const NEW_SECRET = process.env.NEW_SECRET as string;

if (!OLD_SECRET || !NEW_SECRET) {
console.error(
"❌ OLD_SECRET and NEW_SECRET environment variables are required.",
);
console.error(
" Usage: OLD_SECRET=<old> NEW_SECRET=<new> npx tsx apps/dokploy/scripts/migrate-auth-secret.ts",
);
process.exit(1);
}

if (OLD_SECRET === NEW_SECRET) {
console.error("❌ OLD_SECRET and NEW_SECRET must be different.");
process.exit(1);
}

async function reEncrypt(
value: string,
oldSecret: string,
newSecret: string,
): Promise<string> {
const plaintext = await symmetricDecrypt({ key: oldSecret, data: value });
return symmetricEncrypt({ key: newSecret, data: plaintext });
}

async function main() {
console.log("🔍 Fetching 2FA records...");
const records = await db.select().from(twoFactor);

if (records.length === 0) {
console.log("✅ No 2FA records found, nothing to migrate.");
return;
}

console.log(`📦 Found ${records.length} 2FA record(s) to migrate.`);

let migrated = 0;
let failed = 0;

await db.transaction(async (tx) => {
for (const record of records) {
try {
const [newSecret, newBackupCodes] = await Promise.all([
reEncrypt(record.secret, OLD_SECRET, NEW_SECRET),
reEncrypt(record.backupCodes, OLD_SECRET, NEW_SECRET),
]);

await tx
.update(twoFactor)
.set({ secret: newSecret, backupCodes: newBackupCodes })
.where(eq(twoFactor.id, record.id));

migrated++;
} catch (err) {
console.error(
`❌ Failed to migrate record ${record.id} (userId: ${record.userId}):`,
err,
);
failed++;
throw err; // rollback the whole transaction
}
}
});

console.log(`✅ Migrated ${migrated} record(s) successfully.`);

if (failed > 0) {
console.error(
`❌ ${failed} record(s) failed — transaction was rolled back.`,
);
process.exit(1);
} else {
process.exit(0);
}
}

main().catch((err) => {
console.error("❌ Migration failed:", err);
process.exit(1);
});
2 changes: 1 addition & 1 deletion apps/dokploy/server/wss/listen-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const setupDeploymentLogsWebSocketServer = (
sshClient
.on("ready", () => {
const encodedPath = encodeBase64(logPath);
const command = `tail -n +1 -f "$(echo '${encodedPath}' | base64 -d)"`;
const command = `tail -n +1 -f "$(echo '${encodedPath}' | base64 -d)"`;

sshClient!.exec(command, (err, stream) => {
if (err) {
Expand Down
5 changes: 0 additions & 5 deletions packages/server/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,6 @@ const getDockerConfig = (): Docker => {

export const docker = getDockerConfig();

// When not set, use the legacy default so 2FA remains working for users who
// enabled it before BETTER_AUTH_SECRET was introduced.
export const BETTER_AUTH_SECRET =
process.env.BETTER_AUTH_SECRET || "better-auth-secret-123456789";

export const paths = (isServer = false) => {
const BASE_PATH =
isServer || process.env.NODE_ENV === "production"
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/db/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const {
POSTGRES_PORT = "5432",
} = process.env;

function readSecret(path: string): string {
export function readSecret(path: string): string {
try {
return fs.readFileSync(path, "utf8").trim();
} catch {
Expand Down
28 changes: 28 additions & 0 deletions packages/server/src/lib/auth-secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { readSecret } from "../db/constants";

const HARDCODED_LEGACY_SECRET = "better-auth-secret-123456789";

const { BETTER_AUTH_SECRET, BETTER_AUTH_SECRET_FILE } = process.env;

function resolveBetterAuthSecret(): string {
if (BETTER_AUTH_SECRET) {
return BETTER_AUTH_SECRET;
}
if (BETTER_AUTH_SECRET_FILE) {
return readSecret(BETTER_AUTH_SECRET_FILE);
}
if (process.env.NODE_ENV !== "test") {
console.warn(`
⚠️ [DEPRECATED AUTH CONFIG]
BETTER_AUTH_SECRET is not set via environment variable or Docker secret.
Falling back to the insecure hardcoded default — this is a CRITICAL SECURITY RISK.
This mode WILL BE REMOVED in a future release.

Please migrate to Docker Secrets:
curl -sSL https://dokploy.com/security/0.29.3.sh | bash
`);
}
return HARDCODED_LEGACY_SECRET;
}

export const betterAuthSecret = resolveBetterAuthSecret();
6 changes: 4 additions & 2 deletions packages/server/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { APIError } from "better-auth/api";
import { admin, organization, twoFactor } from "better-auth/plugins";
import { and, desc, eq } from "drizzle-orm";
import { BETTER_AUTH_SECRET, IS_CLOUD } from "../constants";
import { IS_CLOUD } from "../constants";
import { db } from "../db";
import * as schema from "../db/schema";
import {
Expand All @@ -27,6 +27,7 @@ import {
} from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils";
import { ac, adminRole, memberRole, ownerRole } from "./access-control";
import { betterAuthSecret } from "./auth-secret";

const { handler, api } = betterAuth({
database: drizzleAdapter(db, {
Expand All @@ -38,8 +39,9 @@ const { handler, api } = betterAuth({
"/organization/create",
"/organization/update",
"/organization/delete",
...(!IS_CLOUD ? ["/verify-email"] : []),
],
secret: BETTER_AUTH_SECRET,
secret: betterAuthSecret,
...(!IS_CLOUD
? {
advanced: {
Expand Down
4 changes: 3 additions & 1 deletion packages/server/src/services/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,9 @@ export const uploadFileToContainer = async (
}

if (!destinationPathRegex.test(destinationPath)) {
throw new Error("Invalid destination path: shell metacharacters are not allowed");
throw new Error(
"Invalid destination path: shell metacharacters are not allowed",
);
}

const normalizedPath = destinationPath.startsWith("/")
Expand Down
Loading