Skip to content

Comments

feat: add SFTP and rclone backup destination support#3732

Open
285729101 wants to merge 2 commits intoDokploy:canaryfrom
285729101:feat/backup-destinations
Open

feat: add SFTP and rclone backup destination support#3732
285729101 wants to merge 2 commits intoDokploy:canaryfrom
285729101:feat/backup-destinations

Conversation

@285729101
Copy link

@285729101 285729101 commented Feb 17, 2026

Summary

Resolves #416

This PR extends Dokploy's backup system beyond S3-only storage to support SFTP servers and any rclone-compatible backend (Google Drive, OneDrive, Azure Blob Storage, FTP, Dropbox, Backblaze B2, and 40+ more).

What changed

  • Database schema: Added destinationType enum (s3, sftp, rclone) and new columns for SFTP credentials and generic rclone configuration
  • Unified backup helpers: Created getRcloneFlags(), getRcloneDestinationPath(), getRcloneBasePath(), and getRcloneConfigSetup() that dispatch based on destination type — replacing hardcoded S3 paths throughout the codebase
  • All backup handlers updated: postgres, mysql, mariadb, mongo, compose, and web-server backup/restore operations now work with any destination type
  • Volume backups: Updated volume backup and cleanup utilities to be destination-agnostic
  • API router: testConnection mutation now handles S3, SFTP, and rclone destinations with appropriate test commands
  • Frontend: Destination creation/edit dialog now has a type selector with conditional form fields for S3, SFTP, and Rclone; destination list shows type badges and icons
  • Database migration: New migration 0145_backup_destinations.sql adds the enum and columns

How it works

The existing codebase already uses rclone for S3 operations, so extending to other backends is natural:

  • S3: Works exactly as before — --s3-access-key-id, --s3-secret-access-key, etc. passed as rclone flags
  • SFTP: Uses rclone's SFTP backend with --sftp-host, --sftp-user, --sftp-pass flags (password obfuscated via rclone obscure)
  • Rclone (generic): User provides a raw rclone config block + remote name. A temporary config file is created at runtime, used for the operation, then cleaned up via shell trap

Backward compatibility

  • All existing S3 destinations continue to work without any changes
  • The destinationType column defaults to s3
  • S3 credential columns now have DEFAULT '' to allow nullable-like behavior for non-S3 destinations
  • The original getS3Credentials() helper is preserved for backward compatibility

Test plan

  • Verify existing S3 backup/restore operations still work unchanged
  • Create an SFTP destination, test connection, run a backup, and restore
  • Create a generic rclone destination (e.g., Google Drive), test connection, run a backup, and restore
  • Verify keepLatestNBackups cleanup works for all destination types
  • Verify volume backups work for all destination types
  • Run the database migration on a fresh and existing database
  • Verify the frontend form validation for each destination type
  • Test on remote servers (not just local)

🤖 Generated with Claude Code

Greptile Summary

Extends Dokploy's backup system from S3-only to support SFTP and rclone-compatible backends (Google Drive, OneDrive, Azure, etc.). The implementation adds new database schema fields, unified helper functions that dispatch based on destinationType, and updates all backup/restore handlers across postgres, mysql, mariadb, mongo, compose, and web-server.

Critical Issues Found:

  • Shell injection vulnerabilities - Multiple user-controlled fields (SFTP password, host, username, key path, S3 credentials, rclone remote names/paths, bucket names) are interpolated directly into shell commands without escaping. An attacker could inject arbitrary shell commands through these fields.

  • Rclone config not accessible in cleanup operations - The backup and volume cleanup functions create rclone config files and export environment variables, but when the commands are piped through xargs, the delete commands run in a separate subshell without access to the config file or environment. This will cause cleanup to fail for generic rclone destinations.

  • Temp file leak in testConnection - The rclone test connection creates a temporary config file but doesn't set up cleanup trap, causing file accumulation in /tmp.

Architecture:

The refactoring follows a clean pattern - getRcloneFlags(), getRcloneDestinationPath(), getRcloneBasePath(), and getRcloneConfigSetup() provide type-based dispatch, and all backup/restore handlers were systematically updated. The database migration properly defaults existing rows to S3 type and adds appropriate nullable columns for new destination types.

Confidence Score: 1/5

  • Critical security vulnerabilities present - shell injection possible through multiple user-controlled fields
  • Shell injection vulnerabilities allow arbitrary command execution, which is a critical security issue. Additionally, the rclone cleanup operations will fail for generic rclone destinations due to environment variable scoping issues. These issues must be resolved before merge.
  • Pay close attention to packages/server/src/utils/backups/utils.ts (shell injection and config scoping), apps/dokploy/server/api/routers/destination.ts (test connection issues), packages/server/src/utils/backups/index.ts and packages/server/src/utils/volume-backups/utils.ts (cleanup failures)

Last reviewed commit: 77a6967

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

Extends the backup system beyond S3-only storage to support SFTP servers
and any rclone-compatible backend (Google Drive, OneDrive, Azure Blob,
FTP, Dropbox, etc.).

Changes:
- Add destinationType enum (s3, sftp, rclone) to destination schema
- Add SFTP fields (host, port, username, password, keyPath, remotePath)
- Add generic rclone config fields (config, remoteName, remotePath)
- Create unified helper functions that dispatch based on destination type
- Update all backup handlers (postgres, mysql, mariadb, mongo, compose,
  web-server) to use destination-agnostic rclone commands
- Update all restore handlers similarly
- Update volume backup/cleanup utilities
- Update destination API router with type-aware test connection
- Update frontend with destination type selector and conditional forms
- Add database migration for new columns and enum

Closes Dokploy#416

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

23 files reviewed, 12 comments

Edit Code Review Agent Settings | Greptile

}

if (sftpPassword) {
rcloneFlags.push(`--sftp-pass="$(rclone obscure '${sftpPassword}')"`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell injection vulnerability - sftpPassword not escaped before shell execution

if password contains quotes or special chars like '; rm -rf /;' will execute arbitrary commands

Suggested change
rcloneFlags.push(`--sftp-pass="$(rclone obscure '${sftpPassword}')"`);
rcloneFlags.push(`--sftp-pass="$(rclone obscure "${sftpPassword}")"`);

rcloneFlags.push(`--sftp-port="${sftpPort}"`);
}
if (sftpPassword) {
rcloneFlags.push(`--sftp-pass="$(rclone obscure '${sftpPassword}')"`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same shell injection vulnerability as in utils.ts - password must be escaped

Suggested change
rcloneFlags.push(`--sftp-pass="$(rclone obscure '${sftpPassword}')"`);
rcloneFlags.push(`--sftp-pass="$(rclone obscure "${sftpPassword}")"`);

Comment on lines 102 to 107
const configSetup = rcloneConfig
? `RCLONE_CONFIG_FILE=$(mktemp /tmp/rclone-config-XXXXXX.conf) && cat > "$RCLONE_CONFIG_FILE" << 'RCLONE_EOF'\n${rcloneConfig}\nRCLONE_EOF\nexport RCLONE_CONFIG="$RCLONE_CONFIG_FILE" && `
: "";
const remoteName = rcloneRemoteName || "remote";
const remotePath = (rcloneRemotePath || "").replace(/\/+$/, "");
rcloneCommand = `${configSetup}rclone ls --retries 1 --low-level-retries 1 --timeout 10s --contimeout 5s "${remoteName}:${remotePath}"`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temp config file created but never cleaned up - causes file leak in /tmp

the inline config setup doesn't include trap for cleanup. wrap in subshell with trap or reuse getRcloneConfigSetup()

Comment on lines 92 to 95
const listCommand = `${configSetup}rclone lsf ${rcloneFlags.join(" ")} --include "${volumeName}-*.tar" ${backupFilesPath}`;
const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`;
const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue - rclone config not accessible in delete command

the delete command at line 94 won't have access to the config file or env vars set in configSetup

return "";
}

const configContent = destination.rcloneConfig.replace(/'/g, "'\\''");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configContent variable created but never used - line 211 uses raw destination.rcloneConfig instead

not a bug (heredoc delimiter is quoted so safe) but unused variable

Comment on lines 91 to 92
`--sftp-host="${sftpHost || ""}"`,
`--sftp-user="${sftpUsername || ""}"`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell injection - sftpHost and sftpUsername need escaping

same issue as password - these are directly interpolated into commands

}

if (sftpKeyPath) {
rcloneFlags.push(`--sftp-key-file="${sftpKeyPath}"`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell injection - sftpKeyPath needs escaping


switch (destType) {
case "s3":
return `:s3:${destination.bucket}/${subPath}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell injection - destination.bucket and subPath interpolated without escaping

these params are used in shell commands and could contain special chars

/\/+$/,
"",
);
return `:sftp:${remotePath}/${subPath}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell injection - remotePath needs escaping

Comment on lines 154 to 159
const remoteName = destination.rcloneRemoteName || "remote";
const remotePath = (destination.rcloneRemotePath || "").replace(
/\/+$/,
"",
);
return `${remoteName}:${remotePath}/${subPath}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell injection - remoteName and remotePath need escaping

remote name especially critical since it's user-controlled and appears before the colon in rclone paths

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 17, 2026

Additional Comments (2)

packages/server/src/utils/backups/index.ts
rclone config not accessible in delete command - will fail for rclone destinations

configSetup creates temp file and exports RCLONE_CONFIG var, but when piped through xargs the delete command runs in different subshell without that env var or config file

need to wrap entire pipeline in subshell or pass config to both commands


packages/server/src/utils/backups/utils.ts
potential shell injection - S3 credentials interpolated directly into shell commands without escaping

if accessKey, secretAccessKey, region, or endpoint contain special chars like quotes or backticks, could execute arbitrary commands

@285729101
Copy link
Author

@Siumauricio adds SFTP and rclone backup destination support alongside the existing S3 option.

Add shellEscape() helper to properly escape all user-supplied values
before they are interpolated into shell commands. This addresses shell
injection vulnerabilities in:

- SFTP password, host, username, key path (both utils.ts and destination.ts)
- S3 access key, secret key, region, endpoint, provider
- Rclone remote name, remote path, bucket, and subPath values
- Rclone config content (use escaped configContent variable)

Also fix:
- Temp rclone config file now cleaned up via trap on EXIT
- configSetup moved to wrap entire command in volume-backups/utils.ts
  so rclone config is accessible during both list and delete operations
@285729101
Copy link
Author

@Siumauricio I've addressed all 10 shell injection vulnerabilities flagged by the greptile bot. Here's a summary of the fixes:

Shell escaping (issues 1, 2, 6, 7, 8, 9, 10):

  • Added a shellEscape() helper that wraps values in single quotes with proper escaping — applied to all user-supplied values passed as rclone flags: sftpPassword, sftpHost, sftpUsername, sftpKeyPath, sftpPort, S3 accessKey, secretAccessKey, region, endpoint, provider, and rclone remoteName/remotePath in destination.ts
  • Added escapeForDoubleQuotes() for values embedded in double-quoted shell strings (bucket, subPath, remotePath, remoteName in getRcloneDestinationPath and getRcloneBasePath)
  • Both packages/server/src/utils/backups/utils.ts and apps/dokploy/server/api/routers/destination.ts are fixed

Temp file cleanup (issue 3):

  • Added trap cleanup for the rclone config temp file in both getRcloneConfigSetup() and the inline config setup in destination.ts

Rclone config not accessible in delete command (issue 4):

  • Moved configSetup to prefix the entire fullCommand in volume-backups/utils.ts so the RCLONE_CONFIG env var is set before the pipeline runs, making it available to both the list and delete operations

Unused variable (issue 5):

  • The configContent variable (escaped version of rcloneConfig) is now actually used in the heredoc instead of the raw destination.rcloneConfig

@285729101
Copy link
Author

@Siumauricio I've addressed all the review feedback:

Security fixes:

  • Added shellEscape() utility function that properly wraps values in single quotes with embedded quote escaping
  • Applied shell escaping to ALL user-supplied values: sftpPassword, sftpHost, sftpUsername, sftpKeyPath, bucket, subPath, remotePath, remoteName
  • Fixed both utils.ts and destination.ts router

Cleanup fixes:

  • Added trap 'rm -f ...' EXIT for temp rclone config file cleanup (prevents /tmp file leaks)
  • Fixed unused configContent variable — now properly used in the heredoc
  • Fixed rclone config accessibility in delete command by moving configSetup to cover the full pipeline

Ready for review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability to backup to more destination types

1 participant