Plain FTP sends your password and every file in clear text. Anyone on the path can read both. So this guide builds an FTP server that only speaks FTPS (FTP over TLS) and rejects everything else.
Here's what you get at the end:
- vsftpd from the official Ubuntu repository, with security updates from Canonical;
- one FTP user locked into its own directory (chroot);
- logins and data transfers that must be encrypted, plain FTP gets
530; - passive mode on a fixed port range, opened in ufw;
- a working setup behind NAT, for example on AWS EC2.
All commands were tested on Ubuntu 24.04 LTS and 26.04 LTS with vsftpd 3.0.5.
If you don't specifically need FTP (a legacy app, a printer or scanner, a partner who only speaks FTP), skip to the SFTP section. It's simpler and you already have OpenSSH on the server.
How FTP works
FTP (File Transfer Protocol) is one of the oldest ways to move files between machines. It uses two connections:
- Control connection on TCP port 21. Commands like
USER,LISTandSTORand the server's replies go here. - Data connection for every file and directory listing. Where it goes depends on the mode.
In active mode the client opens a port and the server connects back to it from port 20. That breaks as soon as the client is behind NAT or a firewall, which is almost always.
In passive mode the server opens a port from a range you choose and the client connects to it. This is what every modern client uses by default, and it's why you need to open a port range in the firewall, not just port 21.
FTP can resume an interrupted transfer, but only if the client supports it (the REST command). Most clients, including FileZilla, lftp and curl, do.
FTPS vs SFTP. The names look alike, but they are different protocols:
- FTPS is regular FTP wrapped in TLS. Same ports, same two connections, needs a certificate.
- SFTP is a file transfer subsystem of SSH. One connection on port 22, no certificates, no passive ports.
Which FTP server to use on Ubuntu
Ubuntu has three common FTP servers: vsftpd, Pure-FTPd and ProFTPD. This guide uses vsftpd:
- it's in the
mainrepository, so Canonical ships its security updates. Pure-FTPd is inuniverse, maintained by the community; - Ubuntu's own FTP server documentation uses it;
- it's small, has one config file, and its defaults are safe: no anonymous access, no writes.
Already running Pure-FTPd? There's a section with the right TLS settings for it further down.
Install and secure vsftpd on Ubuntu
You need an Ubuntu 24.04 or 26.04 server, a user with sudo, and SSH access. Run all commands on the server.
Step 1. Install vsftpd
shell
1sudo apt update2sudo apt install vsftpdThe service starts right away with the default config. That's fine: by default it doesn't allow anonymous logins or uploads. You'll replace the config in step 4.
Step 2. Create an FTP user
Create a separate user for FTP. Don't use root, your own admin account or www-data.
shell
1sudo adduser --shell /usr/sbin/nologin ftpuseradduser asks for a password. This is the password for FTP logins.
The /usr/sbin/nologin shell blocks SSH shell access for this user. vsftpd only lets in users whose shell is listed in /etc/shells, so add it there:
shell
1grep -qx /usr/sbin/nologin /etc/shells || echo /usr/sbin/nologin | sudo tee -a /etc/shellsNow create the folder the user will be locked into. vsftpd refuses to chroot into a directory the user can write to, so the top folder belongs to nobody and is read-only, and uploads go into a subfolder:
shell
1sudo mkdir -p /home/ftpuser/ftp/files2sudo chown nobody:nogroup /home/ftpuser/ftp3sudo chmod a-w /home/ftpuser/ftp4sudo chown ftpuser:ftpuser /home/ftpuser/ftp/filesFinally, put the user on the allow list. Only users in this file will be able to log in:
shell
1echo ftpuser | sudo tee /etc/vsftpd.userlistTo add more FTP users later, repeat this step for each one and add each name on a new line in /etc/vsftpd.userlist.
Step 3. Get a TLS certificate
FTPS needs a certificate, just like HTTPS.
If the server has a domain name (for example ftp.example.com), get a free Let's Encrypt certificate with certbot. Our Let's Encrypt on Ubuntu guide walks through it. On an FTP-only server without a web server, use sudo certbot certonly --standalone -d ftp.example.com: certbot answers the HTTP-01 check on port 80 itself. Then use these paths in step 4:
shell
1rsa_cert_file=/etc/letsencrypt/live/ftp.example.com/fullchain.pem2rsa_private_key_file=/etc/letsencrypt/live/ftp.example.com/privkey.pemCertbot renews the certificate on its own, but vsftpd needs a restart to pick up the new one. Add a deploy hook:
shell
1echo -e '#!/bin/sh\nsystemctl restart vsftpd' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/restart-vsftpd.sh2sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/restart-vsftpd.shFor an internal server or a test, a self-signed certificate works. Clients will ask you to trust it on the first connection. Replace ftp.example.com with your server's name:
shell
1sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \2 -keyout /etc/ssl/private/vsftpd.key \3 -out /etc/ssl/certs/vsftpd.crt \4 -subj "/CN=ftp.example.com" -addext "subjectAltName=DNS:ftp.example.com"5sudo chmod 600 /etc/ssl/private/vsftpd.keyStep 4. Configure vsftpd for FTPS only
Back up the default config:
shell
1sudo cp /etc/vsftpd.conf /etc/vsftpd.conf.origThen replace it with this one. If you used Let's Encrypt in step 3, change the two rsa_ lines:
shell
1sudo tee /etc/vsftpd.conf > /dev/null <<'EOF'2listen=YES3listen_ipv6=NO4anonymous_enable=NO5local_enable=YES6write_enable=YES7local_umask=0228dirmessage_enable=YES9use_localtime=YES10xferlog_enable=YES11connect_from_port_20=YES12secure_chroot_dir=/var/run/vsftpd/empty13pam_service_name=vsftpd14
15# Only users listed in /etc/vsftpd.userlist can log in16userlist_enable=YES17userlist_file=/etc/vsftpd.userlist18userlist_deny=NO19
20# Lock every user into /home/<user>/ftp21chroot_local_user=YES22user_sub_token=$USER23local_root=/home/$USER/ftp24
25# Passive mode26pasv_enable=YES27pasv_min_port=4000028pasv_max_port=4010029#pasv_address=203.0.113.1030
31# TLS only: plain FTP logins and transfers are refused32ssl_enable=YES33rsa_cert_file=/etc/ssl/certs/vsftpd.crt34rsa_private_key_file=/etc/ssl/private/vsftpd.key35allow_anon_ssl=NO36force_local_logins_ssl=YES37force_local_data_ssl=YES38ssl_tlsv1=NO39ssl_tlsv11=NO40ssl_tlsv12=YES41ssl_tlsv13=YES42ssl_sslv2=NO43ssl_sslv3=NO44require_ssl_reuse=YES45ssl_ciphers=HIGH46EOFWhat the important lines do:
listen=YESandlisten_ipv6=NOmake vsftpd listen on IPv4. Ubuntu's default config does the opposite, and with it thepasv_addressoption for NAT doesn't work (more on that below).userlist_enable,userlist_fileanduserlist_deny=NOturn/etc/vsftpd.userlistinto an allow list. Everyone else, includingroot, gets530 Permission denied.chroot_local_user,user_sub_tokenandlocal_rootlock each user into/home/<user>/ftp. They see it as/and can't go higher.pasv_min_portandpasv_max_portset the passive port range. 101 ports is plenty for a small team. Each open transfer takes one port.force_local_logins_ssl=YESrejects logins without TLS, so the password never travels in clear text.force_local_data_ssl=YESrejects unencrypted data connections, so files and listings are encrypted too. Without it, a client can log in over TLS and then send files in clear text.ssl_tlsv12andssl_tlsv13allow only TLS 1.2 and 1.3.require_ssl_reuse=YESmakes the data connection reuse the TLS session of the control connection, so nobody can hijack the data port. FileZilla, WinSCP, lftp and curl support this.
Step 5. Open the ports in ufw
FTP needs port 21 and the whole passive range. With a Let's Encrypt certificate, also allow port 80: certbot renews over HTTP-01 on that port, and the renewal fails if ufw blocks it. If you can't open port 80, use the DNS challenge instead. If ufw isn't enabled yet, allow SSH first, or you'll lock yourself out:
shell
1sudo ufw allow 22/tcp2sudo ufw allow 21/tcp3sudo ufw allow 40000:40100/tcp4sudo ufw allow 80/tcp5sudo ufw enable6sudo ufw statusExpected output:
shell
1Status: active2
3To Action From4-- ------ ----522/tcp ALLOW Anywhere621/tcp ALLOW Anywhere740000:40100/tcp ALLOW AnywhereIf you open only port 21, login works but every ls, upload and download hangs until it times out. That's the data connection hitting the firewall.
On AWS, Google Cloud, Azure or any cloud with its own firewall, add the same inbound rules there too: TCP 21 and TCP 40000-40100. On AWS that's the instance's security group.
Step 6. Restart vsftpd and test
shell
1sudo systemctl restart vsftpd2sudo systemctl status vsftpdNow test from another machine. curl is the quickest way. First, check that plain FTP is refused:
shell
1curl --user ftpuser ftp://ftp.example.com/shell
1curl: (67) Access denied: 530The server replied 530 Non-anonymous sessions must use encryption. That's what you want.
Now connect over FTPS and upload a file. --ssl-reqd tells curl to require TLS. With a self-signed certificate, copy /etc/ssl/certs/vsftpd.crt from the server and pass it with --cacert. With Let's Encrypt, leave --cacert out.
shell
1echo "hello" > test.txt2curl --ssl-reqd --cacert vsftpd.crt --user ftpuser -T test.txt ftp://ftp.example.com/files/3curl --ssl-reqd --cacert vsftpd.crt --user ftpuser ftp://ftp.example.com/files/shell
1-rw-r--r-- 1 1001 1001 6 Nov 04 13:20 test.txtAdd -v to see the details. You should see AUTH SSL or AUTH TLS, SSL connection using TLSv1.3, PROT P (encrypted data channel) and Entering Extended Passive Mode (|||40019|) with a port from your range.
The user is locked in. cd /etc returns 550 Failed to change directory, and uploads outside files return 553 Could not create file.
In FileZilla, open Site Manager and set:
- Protocol: FTP;
- Encryption: Require explicit FTP over TLS;
- Logon type: Normal, with your FTP user and password.
With a self-signed certificate, FileZilla shows the certificate on first connect. Check the fingerprint and trust it. Don't pick "Only use plain FTP" or "Use explicit FTP over TLS if available": the server will refuse the first one, and the second one silently falls back to plain FTP on servers that allow it.
Running behind NAT or on AWS EC2
On AWS EC2 and most cloud VMs the server only knows its private IP, for example 172.31.5.10. In passive mode vsftpd tells the client which IP to connect to, and by default that's the private one. Some clients ignore it and reuse the server's address, others try the private IP and time out.
Fix it by telling vsftpd the public IP (on AWS, use an Elastic IP so it doesn't change). Uncomment this line in /etc/vsftpd.conf and put your IP there:
shell
1pasv_address=203.0.113.10Then restart vsftpd:
shell
1sudo systemctl restart vsftpd⚠️ This only works with listen=YES and listen_ipv6=NO, as in the config above. With Ubuntu's default listen=NO and listen_ipv6=YES, vsftpd ignores pasv_address and sends 0,0,0,0 as the passive address. We checked it on vsftpd 3.0.5: the same setting returned 227 Entering Passive Mode (0,0,0,0,156,148) with the IPv6 listener and 227 Entering Passive Mode (203,0,113,10,156,99) with the IPv4 one.
Uploading files to a website
The old way to give FTP access to a website was to log in as www-data, the user nginx and PHP-FPM run as. Don't do that. Anyone with the FTP password can then upload a PHP file and run it on your server with the web server's permissions.
A safer setup:
- keep a separate FTP user per site or per person;
- add that user to the
www-datagroup if the web server needs to read the files; - point the user's upload folder only at directories with static files (uploads, assets), not at the folder where PHP runs.
For deploying code, use CI/CD or rsync over SSH instead of FTP. You get a history of what changed and an easy rollback.
Using Pure-FTPd instead
If you already run Pure-FTPd, these are the settings that matter on Ubuntu 24.04 and 26.04 (package pure-ftpd 1.0.50). Debian and Ubuntu keep each option as a separate file in /etc/pure-ftpd/conf.
Pure-FTPd uses virtual users mapped to one system account. Create the account and a folder for the first user:
shell
1sudo apt install pure-ftpd2sudo groupadd ftpgroup3sudo useradd -g ftpgroup -d /dev/null -s /usr/sbin/nologin ftpuser4sudo mkdir -p /srv/ftp/joe5sudo chown -R ftpuser:ftpgroup /srv/ftp/joeAdd the virtual user joe (you'll be asked for a password) and turn on the virtual user database:
shell
1sudo pure-pw useradd joe -u ftpuser -g ftpgroup -d /srv/ftp/joe -m2sudo ln -s /etc/pure-ftpd/conf/PureDB /etc/pure-ftpd/auth/60puredbA new system account gets a UID of 1000 or higher, so the default MinUID of 1000 stays as is. Don't lower it to let system users like www-data (UID 33) log in.
Now the security settings:
shell
1cd /etc/pure-ftpd/conf2echo yes | sudo tee ChrootEveryone3echo no | sudo tee PAMAuthentication4echo no | sudo tee UnixAuthentication5echo "40000 40100" | sudo tee PassivePortRange6echo 3 | sudo tee TLSTLS must be 3. With 2, Pure-FTPd requires TLS for the login but still accepts a clear-text data channel: in our test a client sent PROT C and got the directory listing unencrypted. With 3 the same request gets 521 Data connection cannot be opened with this PROT setting. See the Pure-FTPd TLS notes.
Behind NAT, also add the public IP:
shell
1echo "203.0.113.10" | sudo tee ForcePassiveIPPure-FTPd expects the key and the certificate in one file, /etc/ssl/private/pure-ftpd.pem. A self-signed one:
shell
1sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \2 -keyout /etc/ssl/private/pure-ftpd.pem \3 -out /etc/ssl/private/pure-ftpd.pem \4 -subj "/CN=ftp.example.com" -addext "subjectAltName=DNS:ftp.example.com"5sudo chmod 600 /etc/ssl/private/pure-ftpd.pem6sudo systemctl restart pure-ftpdFor Let's Encrypt, concatenate privkey.pem and fullchain.pem into that file and repeat it in a certbot deploy hook. Open the same ports in ufw as for vsftpd, and test with the same curl commands, using joe as the user.
SFTP: the simpler, safer alternative
If your users can use SFTP, use it. SFTP runs over SSH, so there's nothing new to install, no certificate to renew, and one port (22) instead of 21 plus a passive range. FileZilla, WinSCP and Cyberduck all support it.
Here's how to give a user SFTP-only access, locked into a folder, with no shell. Create a group and a user:
shell
1sudo groupadd sftpusers2sudo useradd -m -g sftpusers -s /usr/sbin/nologin alice3sudo passwd aliceThe chroot folder must belong to root and not be writable by anyone else, just like with vsftpd. The user writes into a subfolder:
shell
1sudo mkdir -p /srv/sftp/alice/upload2sudo chown root:root /srv/sftp /srv/sftp/alice3sudo chmod 755 /srv/sftp /srv/sftp/alice4sudo chown alice:sftpusers /srv/sftp/alice/uploadAdd an SSH config file for the group:
shell
1sudo tee /etc/ssh/sshd_config.d/50-sftp.conf > /dev/null <<'EOF'2Match Group sftpusers3 ChrootDirectory /srv/sftp/%u4 ForceCommand internal-sftp5 AllowTcpForwarding no6 X11Forwarding no7EOFCheck the config and restart SSH (open sessions stay connected):
shell
1sudo sshd -t2sudo systemctl restart sshIf the server only accepts SSH keys (PasswordAuthentication no), put Alice's public key into /home/alice/.ssh/authorized_keys. The key file stays in the home directory, outside the chroot.
Test it from another machine:
shell
1sftp alice@sftp.example.comshell
1sftp> ls -la /2drwxr-xr-x ? root root 4096 Nov 4 13:34 /.3drwxr-xr-x ? root root 4096 Nov 4 13:34 /..4drwxr-xr-x ? 1001 1001 4096 Nov 4 13:34 /upload5sftp> cd upload6sftp> put test.txtAlice sees only her folder. ssh alice@sftp.example.com answers This service allows sftp connections only., and port forwarding is refused. Details on every option are in the sshd_config manual.
Troubleshooting
530 Non-anonymous sessions must use encryption. The client connected without TLS. Set the client to "Require explicit FTP over TLS" or use curl --ssl-reqd. This is the server doing its job.
522 Data connections must be encrypted. The client logged in over TLS but asked for a clear-text data channel (PROT C). Same fix: require TLS for everything in the client.
530 Login incorrect. with the right password. Most often the user's shell isn't in /etc/shells, so the PAM check fails. Add /usr/sbin/nologin as shown in step 2. Also check that the user is in /etc/vsftpd.userlist.
TLS error right after login, like bad record type or SSL_read. vsftpd refused the chroot and sent its error in clear text inside the TLS session: 500 OOPS: vsftpd: refusing to run with writable root inside chroot(). Make the top folder read-only: sudo chmod a-w /home/ftpuser/ftp.
Login works, but listing or uploads hang. The passive ports are blocked. Open 40000:40100/tcp in ufw and in your cloud firewall.
227 Entering Passive Mode (0,0,0,0,...) or the client tries a private IP. Set pasv_address and make sure you have listen=YES and listen_ipv6=NO.
vsftpd.service: Failed after editing the config. Run sudo vsftpd /etc/vsftpd.conf in the foreground to see the error. A typo in an option name stops the service from starting.
All options are documented in the vsftpd.conf manual.
Wrapping up
You now have an FTP server on Ubuntu that only accepts encrypted logins and transfers, keeps each user in their own folder and works through a firewall and NAT. If your users can switch to SFTP, it's the easier option to run.
FTP is usually one small piece of a bigger setup: web servers, storage, backups, access control. If you'd like a second pair of eyes on how your servers are secured, our cloud security and compliance team can review it and fix what's risky.
