From a9244d8988e51c6622fc4b9d996ea0b825829f55 Mon Sep 17 00:00:00 2001 From: Rafael Zendron Date: Tue, 16 Jun 2026 15:44:25 -0300 Subject: [PATCH 1/2] fix(backup): redact S3 credentials from logs and error output (#4621) S3 backup credentials (access key + secret) were logged in plaintext to Dokploy service stdout via logger.info in getBackupCommand() and console.error in keepLatestNBackups(). Any operator with access to service logs could recover S3 credentials. Added redactRcloneCredentials() pure function that masks --s3-access-key-id and --s3-secret-access-key values with [REDACTED]. Applied to both the structured logger call and the error handler. Closes #4621 --- .../backups/redact-credentials.test.ts | 50 +++++++++++++++++++ packages/server/src/utils/backups/index.ts | 3 +- packages/server/src/utils/backups/redact.ts | 12 +++++ packages/server/src/utils/backups/utils.ts | 3 +- 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 apps/dokploy/__test__/backups/redact-credentials.test.ts create mode 100644 packages/server/src/utils/backups/redact.ts diff --git a/apps/dokploy/__test__/backups/redact-credentials.test.ts b/apps/dokploy/__test__/backups/redact-credentials.test.ts new file mode 100644 index 0000000000..5fff508cc8 --- /dev/null +++ b/apps/dokploy/__test__/backups/redact-credentials.test.ts @@ -0,0 +1,50 @@ +import { redactRcloneCredentials } from "@dokploy/server/utils/backups/redact"; +import { describe, expect, it } from "vitest"; + +describe("redactRcloneCredentials (#4621)", () => { + it("should redact access key in rclone command", () => { + const cmd = + 'rclone rcat --s3-access-key-id="AKIAIOSFODNN7EXAMPLE" --s3-secret-access-key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" :s3:bucket/file.gz'; + const redacted = redactRcloneCredentials(cmd); + expect(redacted).not.toContain("AKIAIOSFODNN7EXAMPLE"); + expect(redacted).toContain('--s3-access-key-id="[REDACTED]"'); + }); + + it("should redact secret access key in rclone command", () => { + const cmd = + 'rclone rcat --s3-access-key-id="key" --s3-secret-access-key="supersecret" :s3:bucket/file.gz'; + const redacted = redactRcloneCredentials(cmd); + expect(redacted).not.toContain("supersecret"); + expect(redacted).toContain('--s3-secret-access-key="[REDACTED]"'); + }); + + it("should redact both credentials simultaneously", () => { + const cmd = + 'rclone lsf --s3-access-key-id="AKIA123" --s3-secret-access-key="secret456" --s3-region="us-east-1" :s3:bucket/'; + const redacted = redactRcloneCredentials(cmd); + expect(redacted).not.toContain("AKIA123"); + expect(redacted).not.toContain("secret456"); + expect(redacted).toContain('--s3-region="us-east-1"'); + }); + + it("should not modify non-credential flags", () => { + const cmd = + 'rclone rcat --s3-region="eu-west-1" --s3-endpoint="https://s3.example.com" --s3-no-check-bucket :s3:bucket/file.gz'; + const redacted = redactRcloneCredentials(cmd); + expect(redacted).toBe(cmd); + }); + + it("should handle commands with no credentials", () => { + const cmd = "rclone lsf :s3:bucket/"; + expect(redactRcloneCredentials(cmd)).toBe(cmd); + }); + + it("should handle error strings containing credentials", () => { + const errorStr = + 'Error: Command failed: rclone lsf --s3-access-key-id="MYKEY" --s3-secret-access-key="MYSECRET" :s3:bucket/'; + const redacted = redactRcloneCredentials(errorStr); + expect(redacted).not.toContain("MYKEY"); + expect(redacted).not.toContain("MYSECRET"); + expect(redacted).toContain("[REDACTED]"); + }); +}); diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index 8f0e0b6dd9..d0d3045587 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -11,6 +11,7 @@ import { startLogCleanup } from "../access-log/handler"; import { cleanupAll } from "../docker/utils"; import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; +import { redactRcloneCredentials } from "./redact"; import { getS3Credentials, normalizeS3Path, scheduleBackup } from "./utils"; export const initCronJobs = async () => { @@ -153,6 +154,6 @@ export const keepLatestNBackups = async ( await execAsync(rcloneCommand); } } catch (error) { - console.error(error); + console.error(redactRcloneCredentials(String(error))); } }; diff --git a/packages/server/src/utils/backups/redact.ts b/packages/server/src/utils/backups/redact.ts new file mode 100644 index 0000000000..065e76d8cd --- /dev/null +++ b/packages/server/src/utils/backups/redact.ts @@ -0,0 +1,12 @@ +/** + * Redacts S3 credentials from rclone command strings. + * + * Used to prevent credential leakage in structured logs and error output. + * Matches the flag format produced by `getS3Credentials()`: + * --s3-access-key-id="VALUE" and --s3-secret-access-key="VALUE" + */ +export const redactRcloneCredentials = (command: string): string => { + return command + .replace(/(--s3-access-key-id=)"[^"]*"/g, '$1"[REDACTED]"') + .replace(/(--s3-secret-access-key=)"[^"]*"/g, '$1"[REDACTED]"'); +}; diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index 365ebff415..59e0450f93 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -9,6 +9,7 @@ import { runMariadbBackup } from "./mariadb"; import { runMongoBackup } from "./mongo"; import { runMySqlBackup } from "./mysql"; import { runPostgresBackup } from "./postgres"; +import { redactRcloneCredentials } from "./redact"; import { runWebServerBackup } from "./web-server"; export const scheduleBackup = (backup: BackupSchedule) => { @@ -262,7 +263,7 @@ export const getBackupCommand = ( { containerSearch, backupCommand, - rcloneCommand, + rcloneCommand: redactRcloneCredentials(rcloneCommand), logPath, }, `Executing backup command: ${backup.databaseType} ${backup.backupType}`, From 6f6b096e7a558a904a36291132e18a6677efd103 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 5 Jul 2026 16:45:40 -0600 Subject: [PATCH 2/2] fix(backups): redact sensitive information in error logs during web server backup process Updated error handling in the web server backup function to redact Rclone credentials from error messages before logging and notification. This change enhances security by preventing sensitive data exposure in logs. --- .../server/src/utils/backups/web-server.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index 712cc08090..1eaf1981dc 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -11,6 +11,7 @@ import { import { findDestinationById } from "@dokploy/server/services/destination"; import { sendDokployBackupNotifications } from "../notifications/dokploy-backup"; import { execAsync } from "../process/execAsync"; +import { redactRcloneCredentials } from "./redact"; import { getBackupTimestamp, getS3Credentials, normalizeS3Path } from "./utils"; function formatBytes(bytes?: number) { @@ -113,20 +114,23 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { try { await rm(tempDir, { recursive: true, force: true }); } catch (cleanupError) { - console.error("Cleanup error:", cleanupError); + console.error( + "Cleanup error:", + redactRcloneCredentials(String(cleanupError)), + ); } } } catch (error) { - console.error("Backup error:", error); - writeStream.write("Backup error❌\n"); - writeStream.write( - error instanceof Error ? error.message : "Unknown error\n", + const safeErrorMessage = redactRcloneCredentials( + error instanceof Error ? error.message : String(error), ); + console.error("Backup error:", redactRcloneCredentials(String(error))); + writeStream.write("Backup error❌\n"); + writeStream.write(`${safeErrorMessage}\n`); writeStream.end(); await sendDokployBackupNotifications({ type: "error", - // @ts-ignore - errorMessage: error?.message || "Error message not provided", + errorMessage: safeErrorMessage || "Error message not provided", backupSize: formatBytes(computedBackupSize), }); await updateDeploymentStatus(deployment.deploymentId, "error");