This commit is contained in:
Souvik Kumar 2026-09-11 13:11:53 -04:00 committed by GitHub
commit 327471c241
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 145 additions and 10 deletions

View File

@ -0,0 +1,65 @@
import { redactExecError, redactSecrets } from "@dokploy/server/services/registry";
import { ExecError } from "@dokploy/server/utils/process/execAsync";
import { describe, expect, it } from "vitest";
describe("redactSecrets", () => {
it("replaces a secret with ***", () => {
const command = `printf %s 'sup3r-secret' | docker login registry.example.com -u user --password-stdin`;
expect(redactSecrets(command, ["sup3r-secret"])).toBe(
"printf %s '***' | docker login registry.example.com -u user --password-stdin",
);
});
it("redacts every occurrence of a secret", () => {
expect(redactSecrets("pw=abc and again abc", ["abc"])).toBe(
"pw=*** and again ***",
);
});
it("redacts multiple secrets", () => {
expect(redactSecrets("a then b", ["a", "b"])).toBe("*** then ***");
});
it("ignores empty, null and undefined secrets", () => {
const text = "nothing to redact";
expect(redactSecrets(text, ["", null, undefined])).toBe(text);
});
it("leaves the text untouched when the secret is absent", () => {
const text = "no secret here";
expect(redactSecrets(text, ["other"])).toBe(text);
});
it("redacts a shell-escaped password", () => {
const escaped = "'a'\\''b'"; // shEscape("a'b")
const command = `printf %s ${escaped} | docker login`;
expect(redactSecrets(command, [escaped])).toBe(
"printf %s *** | docker login",
);
});
});
describe("redactExecError", () => {
it("redacts the secret from an ExecError message and command", () => {
const error = new ExecError("Command execution failed: pw=hunter2", {
command: "docker login -p hunter2",
stderr: "auth failed for hunter2",
});
const redacted = redactExecError(error, ["hunter2"]);
expect(redacted).toBeInstanceOf(ExecError);
const execError = redacted as ExecError;
expect(execError.message).toBe("Command execution failed: pw=***");
expect(execError.command).toBe("docker login -p ***");
expect(execError.stderr).toBe("auth failed for ***");
});
it("redacts the secret from a plain Error message", () => {
const error = new Error("failed with token abc123");
const redacted = redactExecError(error, ["abc123"]) as Error;
expect(redacted.message).toBe("failed with token ***");
});
it("returns non-error values unchanged", () => {
expect(redactExecError("just a string", ["x"])).toBe("just a string");
});
});

View File

@ -39,6 +39,11 @@ import {
updateDeploymentStatus,
} from "./deployment";
import { type Domain, getDomainHost } from "./domain";
import {
getRegistryPasswords,
redactExecError,
redactSecrets,
} from "./registry";
import {
createPreviewDeploymentComment,
getIssueComment,
@ -246,11 +251,17 @@ export const deployApplication = async ({
});
} catch (error) {
let command = "";
const passwords = await getRegistryPasswords([
application.registryId,
application.buildRegistryId,
application.rollbackRegistryId,
]);
const rawMessage = error instanceof Error ? error.message : String(error);
const safeMessage = redactSecrets(rawMessage, passwords);
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
const encodedMessage = encodeBase64(safeMessage);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
@ -267,13 +278,12 @@ export const deployApplication = async ({
projectName: application.environment.project.name,
applicationName: application.name,
applicationType: "application",
// @ts-ignore
errorMessage: error?.message || "Error building",
errorMessage: safeMessage || "Error building",
buildLink,
organizationId: application.environment.project.organizationId,
});
throw error;
throw redactExecError(error, passwords);
} finally {
// Only extract commit info for non-docker sources
if (application.sourceType !== "docker") {
@ -337,11 +347,16 @@ export const rebuildApplication = async ({
});
} catch (error) {
let command = "";
const passwords = await getRegistryPasswords([
application.registryId,
application.buildRegistryId,
application.rollbackRegistryId,
]);
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
const encodedMessage = encodeBase64(redactSecrets(message, passwords));
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
@ -353,7 +368,7 @@ export const rebuildApplication = async ({
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;
throw redactExecError(error, passwords);
}
return true;
@ -579,11 +594,16 @@ export const rebuildPreviewApplication = async ({
});
} catch (error) {
let command = "";
const passwords = await getRegistryPasswords([
application.registryId,
application.buildRegistryId,
application.rollbackRegistryId,
]);
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
const encodedMessage = encodeBase64(redactSecrets(message, passwords));
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
@ -604,7 +624,7 @@ export const rebuildPreviewApplication = async ({
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",
});
throw error;
throw redactExecError(error, passwords);
}
return true;

View File

@ -3,9 +3,10 @@ import { type apiCreateRegistry, registry } from "@dokploy/server/db/schema";
import {
execAsync,
execAsyncRemote,
ExecError,
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { eq, inArray } from "drizzle-orm";
import type { z } from "zod";
import { IS_CLOUD } from "../constants";
@ -37,6 +38,55 @@ function sanitizeRegistryError(
return message.split(password).join("***");
}
export const redactSecrets = (
text: string,
secrets: (string | null | undefined)[],
): string => {
let result = text;
for (const secret of secrets) {
if (secret) {
result = result.split(secret).join("***");
}
}
return result;
};
export const getRegistryPasswords = async (
registryIds: (string | null | undefined)[],
): Promise<(string | null)[]> => {
const uniqueIds = [...new Set(registryIds.filter((id): id is string => !!id))];
if (uniqueIds.length === 0) {
return [];
}
const rows = await db.query.registry.findMany({
where: inArray(registry.registryId, uniqueIds),
columns: { password: true },
});
return rows.flatMap((row) =>
row.password ? [row.password, shEscape(row.password)] : [row.password],
);
};
export const redactExecError = (
error: unknown,
secrets: (string | null | undefined)[],
): unknown => {
if (error instanceof ExecError) {
return new ExecError(redactSecrets(error.message, secrets), {
command: redactSecrets(error.command, secrets),
stdout: error.stdout ? redactSecrets(error.stdout, secrets) : error.stdout,
stderr: error.stderr ? redactSecrets(error.stderr, secrets) : error.stderr,
exitCode: error.exitCode,
originalError: undefined,
serverId: error.serverId,
});
}
if (error instanceof Error) {
error.message = redactSecrets(error.message, secrets);
}
return error;
};
export const createRegistry = async (
input: z.infer<typeof apiCreateRegistry>,
organizationId: string,