How to Install Ubuntu FTP Server

Published Updated

12 min read

A step-by-step guide to running an FTP server on Ubuntu 24.04 or 26.04 that refuses plain FTP. You'll install vsftpd, lock users into their own folder, require TLS for logins and file transfers, open passive ports in ufw, make it work behind NAT on AWS, and test it with curl and FileZilla. At the end there's a short Pure-FTPd variant and an SFTP setup, which is the better choice if you only need to move files over SSH.


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, LIST and STOR and 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 main repository, so Canonical ships its security updates. Pure-FTPd is in universe, 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 update
2sudo apt install vsftpd

The 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 ftpuser

adduser 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/shells

Now 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/files
2sudo chown nobody:nogroup /home/ftpuser/ftp
3sudo chmod a-w /home/ftpuser/ftp
4sudo chown ftpuser:ftpuser /home/ftpuser/ftp/files

Finally, 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.userlist

To 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.pem
2rsa_private_key_file=/etc/letsencrypt/live/ftp.example.com/privkey.pem

Certbot 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.sh
2sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/restart-vsftpd.sh

For 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.key

Step 4. Configure vsftpd for FTPS only

Back up the default config:

shell

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

Then 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=YES
3listen_ipv6=NO
4anonymous_enable=NO
5local_enable=YES
6write_enable=YES
7local_umask=022
8dirmessage_enable=YES
9use_localtime=YES
10xferlog_enable=YES
11connect_from_port_20=YES
12secure_chroot_dir=/var/run/vsftpd/empty
13pam_service_name=vsftpd
14
15# Only users listed in /etc/vsftpd.userlist can log in
16userlist_enable=YES
17userlist_file=/etc/vsftpd.userlist
18userlist_deny=NO
19
20# Lock every user into /home/<user>/ftp
21chroot_local_user=YES
22user_sub_token=$USER
23local_root=/home/$USER/ftp
24
25# Passive mode
26pasv_enable=YES
27pasv_min_port=40000
28pasv_max_port=40100
29#pasv_address=203.0.113.10
30
31# TLS only: plain FTP logins and transfers are refused
32ssl_enable=YES
33rsa_cert_file=/etc/ssl/certs/vsftpd.crt
34rsa_private_key_file=/etc/ssl/private/vsftpd.key
35allow_anon_ssl=NO
36force_local_logins_ssl=YES
37force_local_data_ssl=YES
38ssl_tlsv1=NO
39ssl_tlsv11=NO
40ssl_tlsv12=YES
41ssl_tlsv13=YES
42ssl_sslv2=NO
43ssl_sslv3=NO
44require_ssl_reuse=YES
45ssl_ciphers=HIGH
46EOF

What the important lines do:

  • listen=YES and listen_ipv6=NO make vsftpd listen on IPv4. Ubuntu's default config does the opposite, and with it the pasv_address option for NAT doesn't work (more on that below).
  • userlist_enable, userlist_file and userlist_deny=NO turn /etc/vsftpd.userlist into an allow list. Everyone else, including root, gets 530 Permission denied.
  • chroot_local_user, user_sub_token and local_root lock each user into /home/<user>/ftp. They see it as / and can't go higher.
  • pasv_min_port and pasv_max_port set the passive port range. 101 ports is plenty for a small team. Each open transfer takes one port.
  • force_local_logins_ssl=YES rejects logins without TLS, so the password never travels in clear text.
  • force_local_data_ssl=YES rejects 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_tlsv12 and ssl_tlsv13 allow only TLS 1.2 and 1.3.
  • require_ssl_reuse=YES makes 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/tcp
2sudo ufw allow 21/tcp
3sudo ufw allow 40000:40100/tcp
4sudo ufw allow 80/tcp
5sudo ufw enable
6sudo ufw status

Expected output:

shell

1Status: active
2
3To Action From
4-- ------ ----
522/tcp ALLOW Anywhere
621/tcp ALLOW Anywhere
740000:40100/tcp ALLOW Anywhere

If 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 vsftpd
2sudo systemctl status vsftpd

Now 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: 530

The 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.txt
2curl --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.txt

Add -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.10

Then 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-data group 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-ftpd
2sudo groupadd ftpgroup
3sudo useradd -g ftpgroup -d /dev/null -s /usr/sbin/nologin ftpuser
4sudo mkdir -p /srv/ftp/joe
5sudo chown -R ftpuser:ftpgroup /srv/ftp/joe

Add 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 -m
2sudo ln -s /etc/pure-ftpd/conf/PureDB /etc/pure-ftpd/auth/60puredb

A 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/conf
2echo yes | sudo tee ChrootEveryone
3echo no | sudo tee PAMAuthentication
4echo no | sudo tee UnixAuthentication
5echo "40000 40100" | sudo tee PassivePortRange
6echo 3 | sudo tee TLS

TLS 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 ForcePassiveIP

Pure-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.pem
6sudo systemctl restart pure-ftpd

For 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 sftpusers
2sudo useradd -m -g sftpusers -s /usr/sbin/nologin alice
3sudo passwd alice

The 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/upload
2sudo chown root:root /srv/sftp /srv/sftp/alice
3sudo chmod 755 /srv/sftp /srv/sftp/alice
4sudo chown alice:sftpusers /srv/sftp/alice/upload

Add an SSH config file for the group:

shell

1sudo tee /etc/ssh/sshd_config.d/50-sftp.conf > /dev/null <<'EOF'
2Match Group sftpusers
3 ChrootDirectory /srv/sftp/%u
4 ForceCommand internal-sftp
5 AllowTcpForwarding no
6 X11Forwarding no
7EOF

Check the config and restart SSH (open sessions stay connected):

shell

1sudo sshd -t
2sudo systemctl restart ssh

If 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.com

shell

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 /upload
5sftp> cd upload
6sftp> put test.txt

Alice 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.

Alex Kondratiev

Alex Kondratiev

Founder and CEO of ITsyndicate. DevOps engineer with 15+ years in cloud infrastructure, Infrastructure as Code, Kubernetes and automation, focused on secure, scalable and resilient systems.

More about Alex

Stay online.
Stay in command.