Overview
Security is critical when working with FTP, especially in production environments. This guide covers TLS encryption, credential management, and Cloudflare Workers-specific security practices.
Use TLS/FTPS
Always use TLS encryption for FTP connections to protect data in transit:
const client = new FTPClient ( 'ftp.example.com' , {
user: 'username' ,
pass: 'password' ,
secure: true // Enable TLS
});
await client . connect ();
What TLS protects
Control channel encryption
All FTP commands and responses are encrypted after the AUTH TLS handshake.
Data channel encryption
File transfers are encrypted when PROT P (protection level private) is enabled.
Credential protection
Username and password are transmitted securely over the encrypted connection.
Man-in-the-middle prevention
TLS certificate validation prevents attackers from intercepting traffic.
TLS implementation details
The FTPClient implements TLS using Cloudflare’s startTls() method from src/classes/ftp-client.ts:136:
if ( this . opts . secure ) {
if ( ! this . feats . AUTH || ! this . feats . AUTH . includes ( "TLS" )) {
console . warn (
"Server does not advertise STARTTLS yet it was requested. \n Attempting anyways..." ,
);
}
status = await this . command ( Commands . Auth , "TLS" );
this . assertStatus ( StatusCodes . AuthProceed , status , this . conn );
this . reader ?. releaseLock ();
this . reader = undefined ;
this . conn = this . conn . startTls ({
expectedServerHostname: this . host ,
});
// Enable data channel protection
status = await this . command ( Commands . Protection , "P" );
this . assertStatus ( StatusCodes . OK , status , this . conn );
}
If the server doesn’t support TLS (AUTH TLS or PROT P not in FEAT response), the connection attempt will log a warning but continue. Some servers may reject the connection. Always verify your server supports TLS before enabling it.
Credential management
Never hardcode credentials in your code. Use environment variables or Cloudflare Workers secrets.
Cloudflare Workers secrets
Store FTP credentials as secrets in Cloudflare Workers:
Using wrangler
Using dashboard
In your code
# Add secrets using wrangler CLI
wrangler secret put FTP_USER
wrangler secret put FTP_PASS
wrangler secret put FTP_HOST
Go to your Worker in the Cloudflare Dashboard
Navigate to Settings → Variables
Add secrets for FTP_USER, FTP_PASS, and FTP_HOST
Click Encrypt for each secret
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
const client = new FTPClient ( env . FTP_HOST , {
user: env . FTP_USER ,
pass: env . FTP_PASS ,
secure: true
});
await client . connect ();
// ... operations ...
await client . close ();
return new Response ( 'Success' );
}
} ;
TypeScript types for environment
Define types for your environment variables:
interface Env {
FTP_HOST : string ;
FTP_USER : string ;
FTP_PASS : string ;
FTP_PORT ?: string ;
}
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
const client = new FTPClient ( env . FTP_HOST , {
user: env . FTP_USER ,
pass: env . FTP_PASS ,
port: env . FTP_PORT ? parseInt ( env . FTP_PORT ) : 21 ,
secure: true
});
// ...
}
} ;
Cloudflare Workers secrets are encrypted at rest and only decrypted at runtime, providing strong security for sensitive credentials.
Validate server hostnames
The TLS implementation validates the server’s hostname using expectedServerHostname:
this . conn = this . conn . startTls ({
expectedServerHostname: this . host ,
});
This prevents man-in-the-middle attacks by ensuring the certificate matches the hostname you’re connecting to.
Always use the actual server hostname, not an IP address, when using TLS. Certificates are issued for hostnames, not IP addresses.
Restrict access with authentication
Never use anonymous FTP for production systems:
Don't use anonymous
Use authenticated access
// INSECURE - Don't do this in production
const client = new FTPClient ( 'ftp.example.com' );
// Uses default 'anonymous' / 'anonymous' credentials
// SECURE - Use proper credentials
const client = new FTPClient ( 'ftp.example.com' , {
user: env . FTP_USER ,
pass: env . FTP_PASS ,
secure: true
});
Validate and sanitize file paths to prevent directory traversal attacks:
function sanitizePath ( path : string ) : string {
// Remove directory traversal attempts
const clean = path . replace ( / \.\. / g , '' );
// Remove leading slashes to prevent absolute paths
return clean . replace ( / ^ \/ + / , '' );
}
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
const url = new URL ( request . url );
const filename = url . searchParams . get ( 'file' );
if ( ! filename ) {
return new Response ( 'Missing file' , { status: 400 });
}
// Sanitize user input
const safePath = sanitizePath ( filename );
const client = new FTPClient ( env . FTP_HOST , {
user: env . FTP_USER ,
pass: env . FTP_PASS ,
secure: true
});
await client . connect ();
const data = await client . download ( safePath );
await client . close ();
return new Response ( data );
}
} ;
Never pass unsanitized user input directly to FTP methods. Attackers could use ../ sequences to access files outside intended directories.
Rate limiting and abuse prevention
Implement rate limiting to prevent abuse:
import { FTPClient } from 'workerd-ftp' ;
interface Env {
FTP_HOST : string ;
FTP_USER : string ;
FTP_PASS : string ;
RATE_LIMITER : RateLimit ; // Cloudflare Rate Limiting API
}
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
// Rate limit by IP
const ip = request . headers . get ( 'CF-Connecting-IP' ) || 'unknown' ;
const { success } = await env . RATE_LIMITER . limit ({ key: ip });
if ( ! success ) {
return new Response ( 'Rate limit exceeded' , { status: 429 });
}
// Proceed with FTP operation
const client = new FTPClient ( env . FTP_HOST , {
user: env . FTP_USER ,
pass: env . FTP_PASS ,
secure: true
});
await client . connect ();
// ... operations ...
await client . close ();
return new Response ( 'Success' );
}
} ;
Connection cleanup
Always close connections to prevent resource exhaustion:
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
const client = new FTPClient ( env . FTP_HOST , {
user: env . FTP_USER ,
pass: env . FTP_PASS ,
secure: true
});
try {
await client . connect ();
// Perform operations
const files = await client . list ();
return Response . json ({ files });
} catch ( error ) {
console . error ( 'FTP error:' , error );
return new Response ( 'Server error' , { status: 500 });
} finally {
// Always close, even on error
await client . close ();
}
}
} ;
The close() method from src/classes/ftp-client.ts:543:
public async close (): Promise < void > {
await this.lock.lock();
this.conn?.close();
this. conn = undefined ;
this.dataConn?.close();
this. dataConn = undefined ;
this.lock.unlock();
}
Use try-finally blocks to ensure connections are closed even if errors occur during operations.
Don’t expose internal errors to users:
try {
await client . download ( 'file.txt' );
} catch ( error : any ) {
// Log detailed error internally
console . error ( 'FTP download failed:' , {
code: error . code ,
message: error . message ,
timestamp: new Date (). toISOString ()
});
// Return generic error to user
return new Response ( 'File download failed' , { status: 500 });
}
Never return raw FTP error messages to users. They may contain sensitive information like server paths, usernames, or internal IP addresses.
Cloudflare Workers security features
Leverage Cloudflare’s security features:
Web Application Firewall (WAF)
Enable WAF rules to protect your Worker endpoints:
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
// Cloudflare WAF runs before your Worker
// Configure rules in the dashboard
// Additional custom validation
if ( request . method !== 'GET' && request . method !== 'POST' ) {
return new Response ( 'Method not allowed' , { status: 405 });
}
// ... FTP operations ...
}
} ;
DDoS protection
Cloudflare’s DDoS protection is automatic, but you should implement application-level protections:
const MAX_FILE_SIZE = 100 * 1024 * 1024 ; // 100 MB
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
// Check content length
const contentLength = request . headers . get ( 'Content-Length' );
if ( contentLength && parseInt ( contentLength ) > MAX_FILE_SIZE ) {
return new Response ( 'File too large' , { status: 413 });
}
// ... FTP operations ...
}
} ;
IP access control
Restrict access to specific IP addresses:
const ALLOWED_IPS = [ '203.0.113.0' , '198.51.100.0' ];
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
const ip = request . headers . get ( 'CF-Connecting-IP' );
if ( ! ip || ! ALLOWED_IPS . includes ( ip )) {
return new Response ( 'Forbidden' , { status: 403 });
}
// ... FTP operations ...
}
} ;
Audit logging
Log security-relevant events:
interface AuditLog {
timestamp : string ;
action : string ;
user : string ;
ip : string ;
success : boolean ;
error ?: string ;
}
async function logAudit (
env : Env ,
log : AuditLog
) : Promise < void > {
// Send to logging service (e.g., Cloudflare Logs, external SIEM)
await fetch ( 'https://logs.example.com/audit' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( log )
});
}
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
const ip = request . headers . get ( 'CF-Connecting-IP' ) || 'unknown' ;
const startTime = Date . now ();
try {
const client = new FTPClient ( env . FTP_HOST , {
user: env . FTP_USER ,
pass: env . FTP_PASS ,
secure: true
});
await client . connect ();
await client . upload ( 'file.txt' , new Uint8Array ([ 1 , 2 , 3 ]));
await client . close ();
await logAudit ( env , {
timestamp: new Date (). toISOString (),
action: 'upload' ,
user: env . FTP_USER ,
ip ,
success: true
});
return new Response ( 'Success' );
} catch ( error ) {
await logAudit ( env , {
timestamp: new Date (). toISOString (),
action: 'upload' ,
user: env . FTP_USER ,
ip ,
success: false ,
error: String ( error )
});
return new Response ( 'Error' , { status: 500 });
}
}
} ;
Security checklist
Enable TLS
Always set secure: true for production connections.
Use secrets
Store credentials in Cloudflare Workers secrets, never in code.
Validate inputs
Sanitize all user-provided file paths and parameters.
Implement rate limiting
Prevent abuse with rate limiting on FTP operations.
Close connections
Always call client.close() in finally blocks.
Handle errors safely
Don’t expose internal error details to users.
Enable logging
Log security events for audit and monitoring.
Use WAF and DDoS protection
Configure Cloudflare’s security features.
Next steps
Connecting Learn more about connection options
API Reference Explore the complete API