feat: add SFTP and FTP backup destination support#4108
Closed
ljapptest-art wants to merge 1 commit intoDokploy:canaryfrom
Closed
feat: add SFTP and FTP backup destination support#4108ljapptest-art wants to merge 1 commit intoDokploy:canaryfrom
ljapptest-art wants to merge 1 commit intoDokploy:canaryfrom
Conversation
- Add destinationType field to schema (s3, sftp, ftp) - Add SFTP/FTP connection fields (host, port, username, password, remotePath) - Implement secure rclone configuration with environment variable-based password handling - Fix shell injection vulnerability in existing implementations - Update all backup and restore operations to support new destination types - Add database migration for new fields Security improvements: - Passwords are passed via rclone obscure and environment variables - All user inputs are properly escaped for shell safety - No direct string interpolation in shell commands for sensitive data Addresses: Dokploy#416
Comment on lines
+52
to
+83
| // Build a temporary destination object from input | ||
| const tempDestination = { | ||
| ...input, | ||
| destinationId: "temp", | ||
| createdAt: new Date(), | ||
| organizationId: "temp", | ||
| destinationType: input.destinationType, | ||
| // Map input fields based on type | ||
| ...(input.destinationType === "s3" ? { | ||
| accessKey: input.accessKey, | ||
| secretAccessKey: input.secretAccessKey, | ||
| bucket: input.bucket, | ||
| region: input.region, | ||
| endpoint: input.endpoint, | ||
| provider: input.provider, | ||
| additionalFlags: input.additionalFlags, | ||
| } : {}), | ||
| ...(input.destinationType === "sftp" ? { | ||
| host: input.host, | ||
| port: input.port, | ||
| username: input.username, | ||
| password: input.password, | ||
| remotePath: input.remotePath, | ||
| } : {}), | ||
| ...(input.destinationType === "ftp" ? { | ||
| host: input.host, | ||
| port: input.port, | ||
| username: input.username, | ||
| password: input.password, | ||
| remotePath: input.remotePath, | ||
| } : {}), | ||
| } as any; |
Contributor
There was a problem hiding this comment.
Redundant field spreading and unsafe
as any cast
The ...input spread on line 55 already copies every field from the discriminated union onto tempDestination. The three subsequent conditional spreads are therefore no-ops — they just re-assign the same properties. The as any cast is needed only to silence the resulting type mismatch, hiding legitimate type errors.
Both issues can be fixed by constructing the object once with explicit nullable defaults for fields that might be absent in a given union branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds support for SFTP and FTP backup destinations, addressing issue #416.
Features
Implementation Details
Schema Changes
destinationTypefield (s3 | sftp | ftp)host,port,username,password,remotePathSecurity Improvements
Why This Approach is Better
I noticed other PRs for this issue have shell injection vulnerabilities. For example, if a password contains
'; rm -rf /; echo '`, it could break out of the shell quoting and execute arbitrary commands.This PR avoids that by:
rclone obscureto encode the passwordRCLONE_SFTP_PASS,RCLONE_FTP_PASS)Files Changed
packages/server/src/db/schema/destination.ts- Schema with discriminated unionpackages/server/src/utils/backups/utils.ts-getRcloneConfig()with secure handlingpackages/server/src/utils/backups/*.ts- Updated all backup operationspackages/server/src/utils/restore/*.ts- Updated all restore operationsapps/dokploy/server/api/routers/destination.ts- Updated testConnectionapps/dokploy/drizzle/0156_add_destination_types.sql- Database migrationTesting
Closes #416
Greptile Summary
This PR adds SFTP and FTP as new backup destination types alongside the existing S3-compatible backend. The implementation is well-structured: a central
getRcloneConfig()helper replaces the S3-onlygetS3Credentials(), and all backup/restore call sites are updated in a uniform, mechanical way. The password-handling approach — runningrclone obscurein a shell preamble and exposing the result via an environment variable — is the correct way to avoid both shell injection and plaintext credential exposure on the command line. The single-quote escaping in the preamble is implemented correctly.Key observations:
web-server.tslog messages ("Uploaded backup to S3 ✅", "Downloading backup from S3...") even though the paths now support all three destination types.testConnectionhandler indestination.tshas redundant conditional spreading (the...inputspread already covers all fields) and uses anas anycast to suppress the resulting type mismatch.updatemutation's catch block still emits"Error connecting to bucket".portfield is a free-form string with no numeric range validation (1–65535).Confidence Score: 4/5
Safe to merge after addressing the missing frontend UI and minor stale-message/code-quality issues; the core security approach is sound.
All remaining findings are P2: misleading log messages, redundant code in testConnection, a stale error string, and no port-range validation. The core password-handling logic (rclone obscure + env-var preamble with correct single-quote escaping) is implemented correctly and the migration is idempotent. The main gap is the absence of any frontend UI, which makes the feature inaccessible to end users via the web interface.
apps/dokploy/server/api/routers/destination.ts (redundant spreading + stale error message), packages/server/src/utils/backups/web-server.ts (stale S3 log messages), packages/server/src/db/schema/destination.ts (port validation, plaintext password)
Important Files Changed
Comments Outside Diff (4)
packages/server/src/utils/backups/web-server.ts, line 606-608 (link)These log messages still say "S3" even though the function now supports SFTP and FTP destinations. Users will see misleading messages like "Uploaded backup to S3 ✅" when they're actually uploading to an SFTP or FTP server.
The same issue exists in the restore path at
packages/server/src/utils/restore/web-server.ts(~line 807: "Downloading backup from S3...").packages/server/src/db/schema/destination.ts, line 163 (link)The
passwordcolumn persists the SFTP/FTP password in plaintext. Unlike S3 access keys (which are generally purpose-created API credentials), SFTP/FTP passwords are often shared credentials that users reuse across systems. A database dump would expose these credentials directly.Consider storing an encrypted form at rest or at minimum documenting this risk clearly. This is consistent with how S3 secrets are handled today, but worth reconsidering now that personal passwords are involved.
packages/server/src/db/schema/destination.ts, line 210-220 (link)portis az.string()with no further constraints. A user could supply"0","99999", or any non-numeric string. The error they get back from rclone will be cryptic rather than a friendly validation message.Consider adding a Zod refinement to enforce a valid port number (1–65535). The same applies to the FTP schema.
apps/dokploy/server/api/routers/destination.ts, line 193 (link)updatehandlerThe error message still reads
"Error connecting to bucket"even though this handler now deals with SFTP and FTP destinations as well.Reviews (1): Last reviewed commit: "feat: add SFTP and FTP backup destinatio..." | Re-trigger Greptile
(2/5) Greptile learns from your feedback when you react with thumbs up/down!