Migration & Upgrades

Migrating from FTP to SFTP: A Complete Transition Guide

How to replace insecure FTP with SFTP (SSH File Transfer Protocol) — server setup, client configuration, automation scripts, and legacy system compatibility.

Why FTP Is a Security Risk

FTP (File Transfer Protocol, RFC 959) was designed in 1971 — fourteen years before the public internet and decades before encryption was considered necessary. Its core problems are fundamental to the protocol, not fixable with configuration:

  • Credentials transmitted in plaintext — username and password visible to anyone on the network path, including cloud provider employees, ISPs, and packet-sniffers
  • Data transmitted in plaintext — every file transferred is readable in transit
  • Passive mode firewall complexity — FTP uses two connections (control on port 21, data on a negotiated high port), requiring stateful firewall inspection that often fails
  • Compliance violations — PCI DSS Requirement 2.2.7 explicitly prohibits FTP for transmitting cardholder data; HIPAA similarly bans unencrypted file transfer

SFTP (SSH File Transfer Protocol, RFC 4253) is not FTP over SSH — it is a completely different protocol that runs as a subsystem of SSH on port 22. All data is encrypted and authenticated using the same proven cryptography as SSH.

Step 1: SFTP Server Setup

OpenSSH sshd Configuration

SFTP is built into OpenSSH's sshd. Enable and harden it in /etc/ssh/sshd_config:

# /etc/ssh/sshd_config

# Enable SFTP subsystem (usually already present):
Subsystem sftp /usr/lib/openssh/sftp-server

# Create a dedicated SFTP group and restrict their access:
Match Group sftpusers
    ChrootDirectory /data/sftp/%u    # Jail each user to their own directory
    ForceCommand internal-sftp        # Prevent shell access
    AllowTcpForwarding no
    X11Forwarding no
    PermitTunnel no
# Create the SFTP group and add users:
sudo groupadd sftpusers
sudo useradd -m -G sftpusers -s /sbin/nologin ftp_user1
sudo passwd ftp_user1

# Create chroot directory (must be owned by root, not writable by others):
sudo mkdir -p /data/sftp/ftp_user1/uploads
sudo chown root:root /data/sftp/ftp_user1
sudo chmod 755 /data/sftp/ftp_user1
sudo chown ftp_user1:sftpusers /data/sftp/ftp_user1/uploads
sudo chmod 750 /data/sftp/ftp_user1/uploads

# Reload sshd:
sudo systemctl reload sshd

Key-Based Authentication (Preferred)

Password authentication works but key-based auth is significantly more secure:

# Generate an SSH key pair on the client machine:
ssh-keygen -t ed25519 -C '[email protected]' -f ~/.ssh/sftp_key

# Install the public key on the server:
sudo mkdir -p /data/sftp/ftp_user1/.ssh
sudo tee /data/sftp/ftp_user1/.ssh/authorized_keys < ~/.ssh/sftp_key.pub
sudo chown -R ftp_user1:ftp_user1 /data/sftp/ftp_user1/.ssh
sudo chmod 700 /data/sftp/ftp_user1/.ssh
sudo chmod 600 /data/sftp/ftp_user1/.ssh/authorized_keys

Note: The .ssh directory must live *inside* the chroot. With ChrootDirectory /data/sftp/%u, the authorized_keys path is /data/sftp/ftp_user1/.ssh/authorized_keys.

Step 2: Client Migration

GUI Clients

Old FTP ClientSFTP ReplacementConfig Change
FileZilla (FTP)FileZilla (SFTP)Protocol: SFTP, Port: 22
WinSCP (FTP)WinSCP (SFTP)File Protocol: SFTP
Cyberduck (FTP)Cyberduck (SFTP)Open Connection → SFTP

FileZilla SFTP site configuration:

Host:     sftp.your-domain.com
Protocol: SFTP - SSH File Transfer Protocol
Port:     22
Logon:    Key file (browse to .ppk or .pem file)

Command-Line SFTP

# Interactive session:
sftp -i ~/.ssh/sftp_key [email protected]
sftp> put local_file.csv uploads/
sftp> get uploads/report.csv .
sftp> ls uploads/
sftp> bye

# Batch/non-interactive upload (scriptable):
sftp -i ~/.ssh/sftp_key [email protected] << 'EOF'
put /local/data.csv uploads/data.csv
bye
EOF

Automation with Python (paramiko)

import paramiko
from pathlib import Path

def upload_file(hostname: str, username: str, key_path: str,
                local_path: str, remote_path: str) -> None:
    key = paramiko.Ed25519Key.from_private_key_file(key_path)
    transport = paramiko.Transport((hostname, 22))
    transport.connect(username=username, pkey=key)

    with paramiko.SFTPClient.from_transport(transport) as sftp:  # type: ignore
        sftp.put(local_path, remote_path)
        print(f'Uploaded {local_path} → {remote_path}')

    transport.close()

upload_file(
    hostname='sftp.your-domain.com',
    username='ftp_user1',
    key_path='/path/to/sftp_key',
    local_path='/tmp/report.csv',
    remote_path='uploads/report.csv',
)

rsync over SSH

For directory synchronization (replaces ncftp or FTP mirror tools):

# Sync local directory to remote SFTP server:
rsync -avz -e 'ssh -i ~/.ssh/sftp_key' \
  /local/data/ \
  [email protected]:uploads/

# With delete (mirror mode — removes files deleted locally):
rsync -avz --delete -e 'ssh -i ~/.ssh/sftp_key' \
  /local/data/ \
  [email protected]:uploads/

Step 3: Legacy System Compatibility

Some legacy systems (embedded hardware, old ERP software) can only speak FTP and cannot be easily updated. Options:

FTP-to-SFTP Proxy Gateway

Run a local FTP gateway that accepts FTP on the internal network and translates to SFTP outbound:

# Install ftpproxy or use sftpgateway:
pip install sftpgateway

# Config: /etc/sftpgateway.conf
# [server]
# listen_host = 127.0.0.1   # Accept FTP locally only
# listen_port = 2121
# [backend]
# sftp_host = sftp.your-domain.com
# sftp_port = 22
# key_file = /etc/sftpgateway/sftp_key

Dual-Mode Transition Period

Run both servers simultaneously during migration:

Phase 1 (weeks 1-4):  Both FTP and SFTP servers active
                       Monitor: count FTP vs SFTP connections
Phase 2 (weeks 5-8):  FTP server read-only, SFTP for writes
                       Notify remaining FTP clients
Phase 3 (week 9+):    FTP server shut down
                       Block port 21 at firewall

Monitor FTP usage to identify stragglers:

# vsftpd access log analysis:
grep 'OK UPLOAD\|OK DOWNLOAD' /var/log/vsftpd.log | awk '{print $7}' | \
  sort | uniq -c | sort -rn | head -20

Step 4: FTPS — When It Makes Sense

FTPS (FTP + TLS, RFC 4217) is a third option: it adds TLS encryption to the standard FTP protocol. It makes sense when:

  • You have many legacy clients that support FTPS but not SFTP
  • You need to preserve FTP semantics (permissions, list formats) exactly
  • Your team has existing FTP PKI infrastructure

FTPS modes:

ModePortHow It Works
**Implicit FTPS**990TLS is mandatory from connection start (recommended)
**Explicit FTPS** (FTPES)21Plain FTP connection, then `AUTH TLS` upgrade
# vsftpd config for Explicit FTPS (the most common modern setup):
# /etc/vsftpd.conf
ssl_enable=YES
force_local_data_ssl=YES     # Require TLS for data channel
force_local_logins_ssl=YES   # Require TLS for authentication
ssl_tlsv1_2=YES
ssl_tlsv1_3=YES
ssl_sslv2=NO
ssl_sslv3=NO
ssl_ciphers=HIGH             # No weak ciphers
rsa_cert_file=/etc/ssl/certs/ftps.crt
rsa_private_key_file=/etc/ssl/private/ftps.key

SFTP vs FTPS recommendation: Prefer SFTP. It uses a single port (22), works cleanly through firewalls, supports key-based auth natively, and requires no separate TLS certificate management beyond your existing SSH setup.

Related Protocols

More in Migration & Upgrades