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
sudoprivileges 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) | |
|---|---|---|
| Encryption | TLS | SSH |
| Ports | 21 plus a range of passive ports | 22 only |
| Extra software | vsftpd | None, OpenSSH is already installed |
| Typical use | Legacy systems, devices and scripts that only speak FTP | Everything 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
Tip
ftpuserhas a normal shell, which vsftpd requires by default, so it could also log in over SSH. To prevent that, runecho 'DenyUsers ftpuser' | sudo tee /etc/ssh/sshd_config.d/deny-ftpuser.confand thensudo systemctl restart ssh.
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_tokenandlocal_rootplace each user in/home/<user>/ftpand prevent them from browsing the rest of the file system.userlist_deny=NOturns/etc/vsftpd.userlistinto an allow list: accounts that are not listed cannot log in, even with a valid password.pasv_min_portandpasv_max_portfix 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_sslandforce_local_data_sslreject any client that tries to log in or transfer files without TLS.require_ssl_reuse=NOavoids 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'sftpdirectory is writable. Runsudo chmod a-w /home/ftpuser/ftpand keep uploads in thefilessubdirectory.530 Login incorrectwith 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 withgetent 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_iptovsftpd.conf. - FileZilla shows a TLS error on transfers. Check that
require_ssl_reuse=NOis set and restart vsftpd. - SFTP disconnects immediately with
Broken pipe. Runsudo journalctl -u ssh -n 20. A message likebad ownership or modes for chroot directorymeans/srv/sftpor/srv/sftp/sftpuseris 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.
