vsftpd ("very secure FTP daemon") is the most common FTP server on Linux. Plain FTP sends passwords and files unencrypted, so this tutorial configures vsftpd for FTPS (FTP over TLS) only, with each user locked into their own directory. Because SFTP over SSH is simpler to firewall and is supported by the same clients, the second part of the guide sets up SFTP-only accounts with OpenSSH, which is the better choice whenever the other side supports it. Everything runs on Ubuntu 24.04.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges and UFW enabled with SSH allowed.
  • An FTP client that supports explicit TLS, such as FileZilla or lftp.

Replace your_server_ip with your server's public IP address in the commands below.

FTPS or SFTP?

FTPS (vsftpd)SFTP (OpenSSH)
EncryptionTLSSSH
Ports21 plus a range of passive ports22 only
Extra softwarevsftpdNone, OpenSSH is already installed
Typical useLegacy systems, devices and scripts that only speak FTPEverything else

If you only need SFTP, you can skip directly to Step 7.

Step 1 - Installing vsftpd

Install vsftpd from the Ubuntu repositories:

sudo apt update
sudo apt install vsftpd

The service starts automatically with a restrictive default configuration. Check that it is running:

systemctl status vsftpd --no-pager

The output should show active (running).

Step 2 - Creating the FTP user and directory

vsftpd will lock (chroot) each user into a directory they cannot leave. For security reasons vsftpd refuses to start a session if that top-level directory is writable by the user, so the usual layout is a read-only ftp directory with a writable files subdirectory inside.

Create a user for FTP, replacing ftpuser with the name you want:

sudo adduser ftpuser

Create the directory structure in the user's home:

sudo mkdir -p /home/ftpuser/ftp/files
sudo chown nobody:nogroup /home/ftpuser/ftp
sudo chmod a-w /home/ftpuser/ftp
sudo chown ftpuser:ftpuser /home/ftpuser/ftp/files

Verify the ownership:

sudo ls -la /home/ftpuser/ftp
dr-xr-xr-x 3 nobody  nogroup 4096 Sep 25 10:00 .
drwxr-x--- 3 ftpuser ftpuser 4096 Sep 25 10:00 ..
drwxr-xr-x 2 ftpuser ftpuser 4096 Sep 25 10:00 files

Only users listed in /etc/vsftpd.userlist will be allowed to log in. Add the new user to it:

echo "ftpuser" | sudo tee -a /etc/vsftpd.userlist

Step 3 - Creating a TLS certificate

FTPS needs a certificate. A self-signed one encrypts the connection just as well; clients will ask you to trust it the first time. Generate one valid for a year:

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/vsftpd.key \
  -out /etc/ssl/certs/vsftpd.pem \
  -subj "/CN=your_server_ip"

Confirm the private key is only readable by root:

sudo ls -l /etc/ssl/private/vsftpd.key
-rw------- 1 root root 1704 Sep 25 10:02 /etc/ssl/private/vsftpd.key

If the server has a domain name with a Let's Encrypt certificate, you can use /etc/letsencrypt/live/your_domain/fullchain.pem and privkey.pem instead in the next step, and clients will trust it without a warning.

Step 4 - Configuring vsftpd

Back up the default configuration:

sudo cp /etc/vsftpd.conf /etc/vsftpd.conf.orig

Open the file:

sudo nano /etc/vsftpd.conf

Replace its entire content with the following:

# Listen on IPv4 and IPv6
listen=NO
listen_ipv6=YES

# Local users only, no anonymous access
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=022
pam_service_name=vsftpd

# Lock each user into /home/<user>/ftp
chroot_local_user=YES
user_sub_token=$USER
local_root=/home/$USER/ftp
secure_chroot_dir=/var/run/vsftpd/empty

# Only users listed in /etc/vsftpd.userlist may log in
userlist_enable=YES
userlist_file=/etc/vsftpd.userlist
userlist_deny=NO

# Passive mode data ports
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=40100

# Require TLS for logins and data transfers
ssl_enable=YES
rsa_cert_file=/etc/ssl/certs/vsftpd.pem
rsa_private_key_file=/etc/ssl/private/vsftpd.key
allow_anon_ssl=NO
force_local_logins_ssl=YES
force_local_data_ssl=YES
require_ssl_reuse=NO
ssl_ciphers=HIGH

# Logging and messages
xferlog_enable=YES
use_localtime=YES
dirmessage_enable=YES

The important settings are:

  • chroot_local_user, user_sub_token and local_root place each user in /home/<user>/ftp and prevent them from browsing the rest of the file system.
  • userlist_deny=NO turns /etc/vsftpd.userlist into an allow list: accounts that are not listed cannot log in, even with a valid password.
  • pasv_min_port and pasv_max_port fix the range of ports used for data connections in passive mode, the mode all modern clients use. You will open exactly this range in the firewall.
  • force_local_logins_ssl and force_local_data_ssl reject any client that tries to log in or transfer files without TLS.
  • require_ssl_reuse=NO avoids failures with clients, including FileZilla in some versions, that don't reuse the TLS session between the control and data connections.

Restart vsftpd to apply the configuration:

sudo systemctl restart vsftpd

Check that it started and listens on port 21:

sudo ss -tlnp | grep ':21 '
LISTEN 0      32                 *:21              *:*    users:(("vsftpd",pid=6011,fd=3))

If the service does not start, journalctl -u vsftpd -n 20 shows the offending setting.

Step 5 - Opening the firewall

FTP uses port 21 for commands and the passive range for data. Open both:

sudo ufw allow 21/tcp
sudo ufw allow 40000:40100/tcp

Check the rules:

sudo ufw status
To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
21/tcp                     ALLOW       Anywhere
40000:40100/tcp            ALLOW       Anywhere
...

Step 6 - Testing the FTPS connection

From your computer, first confirm that unencrypted logins are refused. curl speaks plain FTP by default:

curl ftp://your_server_ip/ --user ftpuser:your_password
curl: (67) Access denied: 530

The server answers 530 Non-anonymous sessions must use encryption., which is exactly what you want. Now connect with TLS using lftp. The ssl:verify-certificate no setting is only needed because the certificate is self-signed:

lftp -u ftpuser -e "set ftp:ssl-force true; set ssl:verify-certificate no; cd files; put /etc/hostname; ls; bye" your_server_ip
Password:
-rw-r--r--    1 1001     1001           12 Sep 25 10:10 hostname

The file was uploaded over an encrypted connection into the user's files directory.

In FileZilla, open File > Site Manager, create a site with protocol FTP, encryption Require explicit FTP over TLS, logon type Normal, and the user's credentials. Accept the certificate when prompted. You will land in /, which is /home/ftpuser/ftp on the server, and can upload into files.

Step 7 - Creating SFTP-only users with OpenSSH

SFTP is part of OpenSSH, so there is nothing to install. The goal here is an account that can transfer files over SFTP, is locked into its own directory, and cannot open a shell or use SSH tunnels.

Create a group for SFTP-only users and a user in it with no shell:

sudo groupadd sftponly
sudo useradd -g sftponly -s /usr/sbin/nologin -d /uploads -M sftpuser
sudo passwd sftpuser

The home directory /uploads is relative to the chroot, so after login the user starts directly in their uploads folder.

OpenSSH requires the chroot directory and every directory above it to be owned by root and not writable by anyone else. Create the chroot with a writable uploads directory inside:

sudo mkdir -p /srv/sftp/sftpuser/uploads
sudo chown root:root /srv/sftp /srv/sftp/sftpuser
sudo chmod 755 /srv/sftp /srv/sftp/sftpuser
sudo chown sftpuser:sftponly /srv/sftp/sftpuser/uploads

Now tell the SSH server how to treat members of the group. Open the main configuration file:

sudo nano /etc/ssh/sshd_config

Add this block at the very end of the file. A Match block applies to every line after it, so it must come last:

Match Group sftponly
    ChrootDirectory /srv/sftp/%u
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no
    PermitTunnel no

%u expands to the username, and ForceCommand internal-sftp replaces any shell or command the user asks for with the built-in SFTP server.

Check the syntax and restart SSH. Keep your current session open until you have confirmed you can still log in with your admin account:

sudo sshd -t
sudo systemctl restart ssh

sshd -t prints nothing when the configuration is valid.

Step 8 - Testing SFTP

From your computer, connect with the sftp client and upload a file:

sftp sftpuser@your_server_ip
sftp> pwd
Remote working directory: /uploads
sftp> put notes.txt
Uploading notes.txt to /uploads/notes.txt
sftp> cd /
sftp> ls
uploads

The user sees only their chroot. Now try to open a shell with the same account:

ssh sftpuser@your_server_ip
This service allows sftp connections only.
Connection to your_server_ip closed.

In FileZilla, use protocol SFTP - SSH File Transfer Protocol and port 22.

For automated transfers, use an SSH key instead of a password. sshd reads authorized_keys before entering the chroot, so the default location (/uploads/.ssh/authorized_keys on the host) is not a good fit. Add this line inside the Match Group sftponly block:

    AuthorizedKeysFile /etc/ssh/authorized_keys/%u

Then create the directory and paste the client's public key into the user's file, and restart SSH:

sudo mkdir -p /etc/ssh/authorized_keys
sudo nano /etc/ssh/authorized_keys/sftpuser
sudo chmod 644 /etc/ssh/authorized_keys/sftpuser
sudo sshd -t && sudo systemctl restart ssh

Troubleshooting

  • 500 OOPS: vsftpd: refusing to run with writable root inside chroot(). The user's ftp directory is writable. Run sudo chmod a-w /home/ftpuser/ftp and keep uploads in the files subdirectory.
  • 530 Login incorrect with the right password. The user is not in /etc/vsftpd.userlist, or their shell is not listed in /etc/shells, which vsftpd's PAM configuration checks. Check with getent passwd ftpuser.
  • Login works but directory listings time out. The data connection is blocked. Make sure the passive range is open in UFW and in any cloud or provider firewall, and that the client uses passive mode. If the server is behind NAT, add pasv_address=your_server_ip to vsftpd.conf.
  • FileZilla shows a TLS error on transfers. Check that require_ssl_reuse=NO is set and restart vsftpd.
  • SFTP disconnects immediately with Broken pipe. Run sudo journalctl -u ssh -n 20. A message like bad ownership or modes for chroot directory means /srv/sftp or /srv/sftp/sftpuser is not owned by root or is group-writable.

Conclusion

You now have a vsftpd server that only accepts TLS-encrypted, chrooted FTP logins from an allow list, plus SFTP-only accounts that are confined to their own directory and cannot open a shell. Good next steps are to replace the self-signed certificate with a Let's Encrypt one, protect ports 21 and 22 against password guessing with Fail2ban, and move legacy FTP users to SFTP when their software allows it.