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}`,