fix(backup): redact S3 credentials from logs and error output (#4648)

* 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

* 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.

---------

Co-authored-by: Mauricio Siu <siumauricio@icloud.com>
This commit is contained in:
Rafael Dias Zendron
2026-07-05 19:52:03 -03:00
committed by GitHub
parent 38de9ef218
commit f8a3561f1e
5 changed files with 77 additions and 9 deletions

View File

@@ -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)));
}
};

View File

@@ -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]"');
};

View File

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

View File

@@ -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");