This commit is contained in:
weibeu 2026-09-11 13:11:52 -04:00 committed by GitHub
commit 4477be7e5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 81 additions and 5 deletions

View File

@ -0,0 +1,38 @@
import { redactSecrets } from "@dokploy/server/utils/process/redactSecrets";
import { describe, expect, it } from "vitest";
// All key material below is synthetic: these base64 strings decode to the
// literal text "synthetic-test-not-a-real-...-key" and are not real keys.
describe("redactSecrets", () => {
it("redacts a PEM private key block written to /tmp/id_rsa", () => {
const secret = "c3ludGhldGljLXRlc3Qtbm90LWEtcmVhbC1wcml2YXRlLWtleQ==";
const command =
`echo "-----BEGIN OPENSSH PRIVATE KEY-----\n${secret}\n-----END OPENSSH PRIVATE KEY-----" > /tmp/id_rsa;` +
"chmod 600 /tmp/id_rsa;git clone --branch main --depth 1 git@example.com:org/repo /code";
const redacted = redactSecrets(command);
expect(redacted).not.toContain(secret);
expect(redacted).toContain("[REDACTED PRIVATE KEY]");
expect(redacted).toContain("chmod 600 /tmp/id_rsa");
expect(redacted).toContain("git clone --branch main");
});
it("redacts a base64 key piped to base64 -d", () => {
const secret = "c3ludGhldGljLXRlc3Qtbm90LWEtcmVhbC1jZXJ0LWtleQ==";
const command = `echo "${secret}" | base64 -d > "/etc/dokploy/cert.key";`;
const redacted = redactSecrets(command);
expect(redacted).not.toContain(secret);
expect(redacted).toContain('echo "[REDACTED]" | base64 -d');
});
it("leaves commands without secrets untouched", () => {
const command =
"git clone --branch main --depth 1 git@github.com:org/repo.git /tmp/code";
expect(redactSecrets(command)).toBe(command);
});
});

View File

@ -1,3 +1,5 @@
import { redactErrorSecrets, redactSecrets } from "./redactSecrets";
export interface ExecErrorDetails {
command: string;
stdout?: string;
@ -16,13 +18,19 @@ export class ExecError extends Error {
public readonly serverId?: string | null;
constructor(message: string, details: ExecErrorDetails) {
super(message);
super(redactSecrets(message));
this.name = "ExecError";
this.command = details.command;
this.stdout = details.stdout;
this.stderr = details.stderr;
this.command = redactSecrets(details.command);
this.stdout = details.stdout
? redactSecrets(details.stdout)
: details.stdout;
this.stderr = details.stderr
? redactSecrets(details.stderr)
: details.stderr;
this.exitCode = details.exitCode;
this.originalError = details.originalError;
this.originalError = details.originalError
? redactErrorSecrets(details.originalError)
: details.originalError;
this.serverId = details.serverId;
// Maintains proper stack trace for where our error was thrown (only available on V8)

View File

@ -0,0 +1,30 @@
// Dokploy embeds some secrets directly into the shell commands it runs: the
// SSH key written to /tmp/id_rsa when cloning over SSH, and the base64 TLS key
// piped to `base64 -d` when provisioning certificates on a remote server. When
// such a command fails, its ExecError (command/stdout/stderr) is logged, which
// would otherwise persist the secret in plain text. These helpers strip that
// material before it can reach the logs.
const PRIVATE_KEY_BLOCK =
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g;
const BASE64_DECODE_PIPE = /echo "[A-Za-z0-9+/=]+"\s*\|\s*base64 -d/g;
export const redactSecrets = (value: string): string =>
value
.replace(PRIVATE_KEY_BLOCK, "[REDACTED PRIVATE KEY]")
.replace(BASE64_DECODE_PIPE, 'echo "[REDACTED]" | base64 -d');
// Node's child_process errors repeat the failed command on `message`, `stack`
// and `cmd`, so redact those too when wrapping an original error.
export const redactErrorSecrets = <T extends Error>(error: T): T => {
const candidate = error as T & { cmd?: string };
candidate.message = redactSecrets(candidate.message);
if (candidate.stack) {
candidate.stack = redactSecrets(candidate.stack);
}
if (candidate.cmd) {
candidate.cmd = redactSecrets(candidate.cmd);
}
return candidate;
};