This guide covers security best practices for both the FTP client and server implementations.
Always use explicit or implicit FTPS in production. Plain FTP transmits credentials and data in cleartext, making it vulnerable to interception.
Explicit TLS (AUTH TLS) is the recommended mode. The client connects on port 21 and upgrades to TLS:
package main
import (
"crypto/tls"
"log"
"github.com/gonzalop/ftp"
)
func main() {
// Secure client with explicit TLS
client, err := ftp.Dial("ftp.example.com:21",
ftp.WithExplicitTLS(&tls.Config{
ServerName: "ftp.example.com",
MinVersion: tls.VersionTLS12, // Require TLS 1.2 or higher
}),
)
if err != nil {
log.Fatal(err)
}
defer client.Quit()
// Login and use
if err := client.Login("username", "password"); err != nil {
log.Fatal(err)
}
}Implicit TLS wraps the entire connection in TLS from the start, typically on port 990:
client, err := ftp.Dial("ftp.example.com:990",
ftp.WithImplicitTLS(&tls.Config{
ServerName: "ftp.example.com",
MinVersion: tls.VersionTLS12,
}),
)The Connect helper automatically handles TLS based on the URL scheme:
// Explicit TLS
client, err := ftp.Connect("ftp+explicit://user:pass@ftp.example.com")
// Implicit TLS
client, err := ftp.Connect("ftps://user:pass@ftp.example.com:990")
⚠️ WARNING: Never useInsecureSkipVerify: truein production!
client, err := ftp.Dial("ftp.example.com:21",
ftp.WithExplicitTLS(&tls.Config{
ServerName: "ftp.example.com",
MinVersion: tls.VersionTLS12,
// Let Go verify against system CA pool
}),
)For development/testing with self-signed certificates, add the CA to a custom cert pool:
import (
"crypto/x509"
"os"
)
// Load custom CA certificate
caCert, err := os.ReadFile("ca.crt")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
client, err := ftp.Dial("ftp.example.com:21",
ftp.WithExplicitTLS(&tls.Config{
ServerName: "ftp.example.com",
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}),
)// ⚠️ ONLY for development/testing!
client, err := ftp.Dial("ftp.example.com:21",
ftp.WithExplicitTLS(&tls.Config{
InsecureSkipVerify: true, // DO NOT USE IN PRODUCTION
}),
)For mutual TLS authentication, provide a client certificate:
// Load client certificate and key
cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
if err != nil {
log.Fatal(err)
}
client, err := ftp.Dial("ftp.example.com:21",
ftp.WithExplicitTLS(&tls.Config{
ServerName: "ftp.example.com",
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}),
)Never hardcode credentials in source code.
import "os"
username := os.Getenv("FTP_USERNAME")
password := os.Getenv("FTP_PASSWORD")
client, err := ftp.Connect(fmt.Sprintf("ftp://%s:%s@ftp.example.com",
url.QueryEscape(username),
url.QueryEscape(password)))import (
"encoding/json"
"os"
)
type Config struct {
Host string `json:"host"`
Username string `json:"username"`
Password string `json:"password"`
}
// Load from config file (ensure file has 0600 permissions)
data, err := os.ReadFile("ftp-config.json")
var config Config
json.Unmarshal(data, &config)
client, err := ftp.Dial(config.Host)
client.Login(config.Username, config.Password)For production, use dedicated secret management:
- AWS Secrets Manager
- HashiCorp Vault
- Kubernetes Secrets
- Azure Key Vault
package main
import (
"crypto/tls"
"log"
"github.com/gonzalop/ftp/server"
)
func main() {
// Load server certificate
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
log.Fatal(err)
}
// Create driver
driver, err := server.NewFSDriver("/var/ftp")
if err != nil {
log.Fatal(err)
}
// Create server with TLS
srv, err := server.NewServer(":21",
server.WithDriver(driver),
server.WithTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
}),
)
if err != nil {
log.Fatal(err)
}
log.Println("Starting secure FTP server on :21")
log.Fatal(srv.ListenAndServe())
}import "net"
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
srv, _ := server.NewServer(":990",
server.WithDriver(driver),
server.WithTLS(tlsConfig),
)
// Listen with TLS wrapper
ln, _ := tls.Listen("tcp", ":990", tlsConfig)
srv.Serve(ln)Best Practices:
- Use certificates from trusted CAs (Let's Encrypt, DigiCert, etc.)
- Set up automatic certificate renewal
- Monitor certificate expiration
- Use strong key sizes (2048-bit RSA minimum, 4096-bit recommended)
Let's Encrypt Example:
# Use certbot to get certificates
certbot certonly --standalone -d ftp.example.com
# Certificates will be in:
# /etc/letsencrypt/live/ftp.example.com/fullchain.pem
# /etc/letsencrypt/live/ftp.example.com/privkey.pemcert, err := tls.LoadX509KeyPair(
"/etc/letsencrypt/live/ftp.example.com/fullchain.pem",
"/etc/letsencrypt/live/ftp.example.com/privkey.pem",
)Never use default anonymous access in production.
Implement a custom Authenticator for production deployments:
import "net"
driver, err := server.NewFSDriver("/var/ftp/default",
server.WithAuthenticator(func(user, pass, host string, remoteIP net.IP) (string, bool, error) {
// Example: Database-backed authentication
valid, rootPath, readOnly := validateUser(user, pass)
if !valid {
return "", false, fmt.Errorf("authentication failed")
}
return rootPath, readOnly, nil
}),
)Restrict access by IP address using the remoteIP parameter:
import "net"
type IPFilter struct {
allowedNetworks []*net.IPNet
}
func (f *IPFilter) IsAllowed(ip net.IP) bool {
if ip == nil {
return false
}
for _, network := range f.allowedNetworks {
if network.Contains(ip) {
return true
}
}
return false
}
// Parse allowed networks
_, net1, _ := net.ParseCIDR("192.168.1.0/24")
_, net2, _ := net.ParseCIDR("10.0.0.0/8")
filter := &IPFilter{
allowedNetworks: []*net.IPNet{net1, net2},
}
driver, _ := server.NewFSDriver("/var/ftp",
server.WithAuthenticator(func(user, pass, host string, remoteIP net.IP) (string, bool, error) {
// Check if IP is allowed
if !filter.IsAllowed(remoteIP) {
return "", false, fmt.Errorf("IP not allowed")
}
// Continue with credential validation
return validateCredentials(user, pass)
}),
)Additional IP-based examples:
// Block specific IPs
server.WithAuthenticator(func(user, pass, host string, remoteIP net.IP) (string, bool, error) {
blocked := []string{"192.168.1.100", "10.0.0.50"}
for _, ip := range blocked {
if remoteIP.String() == ip {
return "", false, fmt.Errorf("IP blocked")
}
}
return validateCredentials(user, pass)
})
// Allow only localhost
server.WithAuthenticator(func(user, pass, host string, remoteIP net.IP) (string, bool, error) {
if remoteIP != nil && !remoteIP.IsLoopback() {
return "", false, fmt.Errorf("only localhost allowed")
}
return validateCredentials(user, pass)
})Best Practices:
- Hash passwords using bcrypt, scrypt, or Argon2
- Never store plaintext passwords
- Enforce strong password policies
- Consider multi-factor authentication (MFA)
import "golang.org/x/crypto/bcrypt"
// Hash password when creating user
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
// Verify during authentication
err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(providedPassword))
if err != nil {
return "", false, fmt.Errorf("invalid password")
}Protect against password guessing attacks by limiting connection attempts.
srv, _ := server.NewServer(":21",
server.WithDriver(driver),
server.WithMaxConnections(100, 5), // Max 100 total, 5 per IP
)Implement failed login tracking in your authenticator:
import (
"sync"
"time"
)
type FailedLoginTracker struct {
mu sync.Mutex
attempts map[string][]time.Time
}
func (t *FailedLoginTracker) RecordFailure(ip string) {
t.mu.Lock()
defer t.mu.Unlock()
t.attempts[ip] = append(t.attempts[ip], time.Now())
}
func (t *FailedLoginTracker) IsBlocked(ip string) bool {
t.mu.Lock()
defer t.mu.Unlock()
attempts := t.attempts[ip]
// Remove attempts older than 15 minutes
cutoff := time.Now().Add(-15 * time.Minute)
valid := attempts[:0]
for _, attempt := range attempts {
if attempt.After(cutoff) {
valid = append(valid, attempt)
}
}
t.attempts[ip] = valid
// Block if 3+ failed attempts in window
return len(valid) >= 3
}
// Use in authenticator
tracker := &FailedLoginTracker{
attempts: make(map[string][]time.Time),
}
driver, _ := server.NewFSDriver("/var/ftp",
server.WithAuthenticator(func(user, pass, host string, remoteIP net.IP) (string, bool, error) {
ip := remoteIP.String()
if tracker.IsBlocked(ip) {
return "", false, fmt.Errorf("too many failed attempts")
}
valid := validateCredentials(user, pass)
if !valid {
tracker.RecordFailure(ip)
return "", false, fmt.Errorf("invalid credentials")
}
return "/var/ftp", false, nil
}),
)The FSDriver uses Go's os.Root API to enforce a chroot jail, preventing directory traversal attacks:
// FSDriver automatically uses os.Root for security
driver, err := server.NewFSDriver("/var/ftp")
// Users cannot access files outside /var/ftp
// Attempts to access /../etc/passwd will failControl default permissions for uploaded files:
driver, _ := server.NewFSDriver("/var/ftp",
server.WithSettings(&server.Settings{
Umask: 0022, // Files: 0644, Directories: 0755
}),
)driver, _ := server.NewFSDriver("/var/ftp",
server.WithDisableAnonymous(true), // Disable anonymous entirely
// OR
// server.WithAnonWrite(false), // Allow anonymous read-only
)The library includes several built-in protections against Denial of Service (DoS) and resource exhaustion attacks.
To prevent a malicious server from crashing the client via memory exhaustion, the control connection parser enforces the following limits:
- Max Line Length: 4096 bytes. Any single response line exceeding this will result in an error.
- Max Response Lines: 1000 lines. Multi-line responses (like
FEATor large directory listings) are capped to prevent unbounded memory growth.
The server implements limits on potentially expensive operations:
- Recursive Listing Depth:
LIST -Ris limited to a recursion depth of 100. This prevents stack overflow or I/O exhaustion from deeply nested directory structures. - HASH Size Limit: The
HASHcommand (used for verifying file integrity) is limited to files up to 250 MB. Requests for larger files will return a552error to prevent long-running hash operations from blocking the control connection. - Connection Limits: Always use
WithMaxConnections(total, perIP)to prevent connection exhaustion.
Enable IP redaction in logs for privacy compliance:
srv, _ := server.NewServer(":21",
server.WithDriver(driver),
server.WithRedactIPs(true), // "192.168.1.100" → "192.168.1.xxx"
)Redact sensitive information from logged paths:
import (
"strings"
"regexp"
)
srv, _ := server.NewServer(":21",
server.WithDriver(driver),
server.WithPathRedactor(func(path string) string {
// Redact user IDs from paths like /users/12345/file.txt
re := regexp.MustCompile(`/users/\d+/`)
return re.ReplaceAllString(path, "/users/*/")
}),
)Enable standard xferlog format for compliance:
import "os"
logFile, _ := os.OpenFile("/var/log/xferlog",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
srv, _ := server.NewServer(":21",
server.WithDriver(driver),
server.WithTransferLog(logFile),
)- Outbound: Port 21 (control)
- Outbound: High ports for data (server-dependent)
- Inbound: Port 21 (control)
- Inbound: Passive port range (configure with
WithPasvMinPort/WithPasvMaxPort)
srv, _ := server.NewServer(":21",
server.WithDriver(driver),
server.WithPasvMinPort(50000),
server.WithPasvMaxPort(51000),
)Firewall rules:
# Allow FTP control
iptables -A INPUT -p tcp --dport 21 -j ACCEPT
# Allow passive data ports
iptables -A INPUT -p tcp --dport 50000:51000 -j ACCEPTFor internet-facing FTP servers:
- Deploy in DMZ (demilitarized zone)
- Use separate network segment
- Restrict access to internal networks
- Enable comprehensive logging
- Monitor for suspicious activity
- Use FTPS (explicit or implicit TLS)
- Validate server certificates (no
InsecureSkipVerifyin production) - Store credentials securely (environment variables, secret management)
- Use TLS 1.2 or higher
- Implement connection timeouts
- Enable keep-alive for long-running operations
- Log errors and connection issues
- Handle network failures gracefully
- Enable TLS/FTPS with valid certificates
- Disable anonymous access (or restrict to read-only)
- Implement custom authentication
- Set connection limits (
WithMaxConnections) - Configure passive port range for firewall
- Set appropriate file permissions (umask)
- Enable transfer logging for audit trail
- Configure IP redaction for privacy compliance
- Implement brute force protection
- Monitor failed login attempts
- Set up certificate renewal automation
- Enable structured logging
- Configure read/write timeouts
- Test graceful shutdown
- Document security policies
- OWASP FTP Security
- NIST TLS Guidelines
- Let's Encrypt - Free TLS certificates