vsftpd opens two connections for every session, one for commands on port 21 and one for the bytes.
That second connection is where a working login turns into a failed upload, and it is the one worth understanding before you edit anything. I ran the whole sequence on 3.0.5, which both current Ubuntu LTS releases ship, and I kept both refusals it gave me.
What an FTP server does on Ubuntu
An FTP server is a daemon that waits on port 21, authenticates a client, then opens a second connection for each directory listing and each file that moves.
vsftpd is the daemon Ubuntu’s own server documentation walks through, and one package with one configuration file covers the whole setup.

A half-configured server behaves exactly like that. Login only needs the control channel, so it succeeds even when nothing on the data channel can reach the server.
A directory listing that hangs, or a short 425 Failed to establish connection in FileZilla, is the data channel failing while the control channel keeps working.
FTP also sends everything in the clear, passwords included. If you already have SSH on the machine, the SFTP note at the end is the shorter and safer route, and FTPS is the option when a device or a client only speaks FTP.
What you need before the first command
Sort out these four things and the commands below work on your machine.
- A current Ubuntu release. I checked the package on both 26.04 LTS and 24.04 LTS and both carry vsftpd 3.0.5.
- sudo on the server, and an account name you can hand to the people who will upload.
- The address clients will use, either on your local network or the public one.
- Ports 21 and the passive range allowed inwards. On a cloud instance or a machine behind a router, that rule lives at the provider or the router as well, and UFW alone will not open it.
Set up the FTP server with vsftpd
The server is one system account plus one configuration file, and each step below names the gate it opens.
Install the package
Ubuntu does not install an FTP daemon by default, so there is nothing already listening on port 21 to fight with.
sudo apt install vsftpd
The install adds a unit called vsftpd.service and a system account called ftp, whose home directory is /srv/ftp. Anonymous logins are switched off in the shipped file, so that account cannot log in until you change anonymous_enable.
Create the account that will log in
A local account is how vsftpd authenticates by default, and one account per person keeps the log readable.
sudo adduser ftpuser
adduser asks for a password, creates the home directory and sets the shell in one pass, which is why I use it here rather than the lower-level useradd command.
Keep the default shell unless you have a reason not to. vsftpd authenticates through PAM, and the PAM stack checks the account’s shell against the list in /etc/shells before it even looks at the password.
Switch on writing and set the file mode
The shipped configuration lets local accounts log in and read, and it has never allowed a write, because write_enable is commented out and defaults to NO.
write_enable=YES
local_umask=022
local_umask decides the mode of every file an upload creates. Its default is 077, so every upload lands as a 600 file and a web server cannot read what the FTP account just wrote.
I set 022 for the transfer below, which gives new files 644 and keeps the upload readable by everyone on the machine.
Jail the account to its home directory
chroot_local_user keeps a login inside its home directory and hides the rest of the filesystem from the client.
chroot_local_user=YES
Turn it on and the next login fails with a message that names a rule you never typed. Here is the one I got.

The guard rail exists because a writable jail root would let the account create files the daemon then trusts, so vsftpd refuses to chroot into a directory the account can rewrite.
There are two ways out. The one I used takes the write bit off the home directory and gives the account a subdirectory to write in.
sudo chmod a-w /home/ftpuser
sudo mkdir -p /home/ftpuser/files
sudo chown ftpuser:ftpuser /home/ftpuser/files
If the permission letters are new, the chmod and chown guide covers what each position in the mode string controls.
The other fix is one line, allow_writeable_chroot=YES, and it keeps the home directory writable instead of taking the write bit away. The Ubuntu man page for vsftpd.conf does not list it, so I tested it against the binary on both concerns: the daemon accepts the option, and a login into a writable home succeeds once it is set.
Pin the passive ports and open the firewall
By default the data connection lands on a random high port, which no firewall rule can describe without opening everything.
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=40100
With the range fixed, the firewall rule is short, and FileZilla, lftp and the command line client all default to passive mode, so this is the range that carries the transfers.
sudo ufw allow 21/tcp
sudo ufw allow 40000:40100/tcp
The UFW firewall guide has the rule syntax and the order those rules are evaluated in.
On a machine behind a router, open the same range there and set pasv_address to the address the client actually connects to. Without it, vsftpd advertises its private address in the PASV reply and the client tries to reach something it cannot route.
Restart and confirm it is listening
Restart the unit to load the file, then check both the unit state and the socket.
sudo systemctl restart vsftpd
sudo systemctl is-active vsftpd
sudo ss -tlnp | grep :21

active from is-active means the file parsed. An option the binary does not recognise stops the daemon before it binds anything, which is why the listening check comes straight after the restart and not later.
There is more on reading that output in the guide to finding the open ports on a Linux machine, and the service management reference covers the restart verbs if systemctl is new to you.
Move a file over the connection
The command line client in the ftp package needs no configuration, and connecting from the server itself proves the whole path end to end.
sudo apt install ftp
ftp -n 127.0.0.1
The -n flag skips the automatic login so the client asks for the user name instead of sending your local one.

Read the 229 lines in that session. The port in brackets is the data connection, and every one of them sits inside the 40000 to 40100 range from the configuration file.
The upload also landed as 644 rather than the 600 the shipped umask gives you, so local_umask is doing its work. The FTP command reference lists the rest of the client verbs if you want to rename or delete over the session.
Encrypt the session with FTPS
Plain FTP carries the password and every byte in the clear, so on any machine reachable from a network you do not control, the next step is not optional.
Generate a certificate for the server
FTPS is FTP wrapped in TLS, so the server needs a certificate and a private key before it can negotiate anything.
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/vsftpd.pem -out /etc/ssl/private/vsftpd.pem
It writes both halves into the same file and asks for a country, an organisation and a common name. The common name is what a client shows when it warns about the self-signed certificate, so put the host name your clients use there.
For a server that other people reach, the guide to installing a certificate on Linux covers the signed route, which removes that warning entirely.
Turn TLS on
Add the block below to the end of /etc/vsftpd.conf and restart the unit.
ssl_enable=YES
rsa_cert_file=/etc/ssl/private/vsftpd.pem
rsa_private_key_file=/etc/ssl/private/vsftpd.pem
force_local_logins_ssl=YES
force_local_data_ssl=YES
require_ssl_reuse=NO
- ssl_enable switches TLS support on for both connections.
- rsa_cert_file and rsa_private_key_file point at the pair you just generated. Left alone they point at the snakeoil certificate from the ssl-cert package, which every other service on the machine shares.
- force_local_logins_ssl and force_local_data_ssl refuse any account that has not negotiated TLS, on the control channel and on the data channel.
- require_ssl_reuse defaults to YES and breaks a number of clients, so it is set to NO here.
sudo systemctl restart vsftpd
You do not need to name the TLS versions. TLS 1.2 and 1.3 are permitted already, and the options that control them are spelled ssl_tlsv12 and ssl_tlsv13, which is not the spelling you will find in the man page.
Point the client at the encrypted port
The command line ftp client cannot negotiate TLS at all, so with both force options set it never gets past the user name.

lftp and FileZilla both handle FTPS. The session below is the lftp one I ran against the same server, with the data channel encrypted as well.
lftp [email protected]
set ssl:verify-certificate no
set ftp:ssl-force true
cd files
ls
In FileZilla the equivalent setting is the Encryption dropdown in the Site Manager, set to require explicit FTP over TLS. The lftp FTPS walkthrough on this site covers the client side in more depth.
verify-certificate is off in that session because the certificate I generated is self-signed. Once a signed certificate is installed, drop the setting and the client validates the host name for you.
The refusals you will meet, and what each one means
Every message below comes from one specific gate, and knowing which gate it is saves a round of guessing.
| Message | What it means | Fix |
|---|---|---|
| 500 OOPS: vsftpd: refusing to run with writable root inside chroot() | chroot_local_user is on and the account can write to the root of its jail | chmod a-w on the home directory and add a writable subdirectory, or set allow_writeable_chroot=YES |
| 530 Login incorrect | the password is wrong, the account is in /etc/ftpusers, or its shell is not in /etc/shells | fix the password, or add the shell to /etc/shells |
| 553 Could not create file | the target directory is not writable by the account | chown the directory to the account, and check local_umask |
| 425 Failed to establish connection | the data channel was blocked, usually on the passive range | open pasv_min_port to pasv_max_port on every firewall in the path |
| 530 Non-anonymous sessions must use encryption | the client did not negotiate TLS while force_local_logins_ssl is on | connect with an FTPS capable client, or turn the force option off |
| the unit fails with status=2/INVALIDARGUMENT | the configuration file contains an option name the binary does not accept | check the spelling against the binary, not the man page |
That number covers three unrelated causes, and the message never says which one you hit.
I confirmed the shell rule the slow way. A user created with /usr/sbin/nologin as its shell is refused at login, and the same account works the moment /usr/sbin/nologin is added to /etc/shells. vsftpd authenticates through PAM, and the shipped /etc/pam.d/vsftpd carries an auth required pam_shells.so line that checks the list.
If you see that status code on the unit, ask systemd for the exit detail rather than guessing.
systemctl status vsftpd
An exit status of 2 with INVALIDARGUMENT in the line means the file itself is bad, and the daemon stopped before it bound port 21. I hit that once with ssl_tlsv1_2=YES, the spelling the man page prints, which the binary rejects outright.
The other account trap is /etc/ftpusers. It already lists root, daemon, bin and a handful of system accounts, and any name on that list cannot log in however correct the password is.
When FTP is the wrong tool
FTP earns its place when a device or a client only speaks FTP, and it is a second daemon to patch when you already have SSH on the machine.
SFTP runs inside SSH on port 22, needs no daemon of its own, no passive range and no certificate, and it encrypts everything by default. On a server you already administer over SSH, it is one command with nothing to configure.
sftp [email protected]
If you want a client with more than a prompt, the SFTP client guide covers the graphical options.
Frequently asked questions
Does Ubuntu have an FTP server installed by default?
No. Ubuntu ships no FTP daemon, so nothing listens on port 21 until you install vsftpd or another server such as proftpd or pure-ftpd. The vsftpd package is the one Ubuntu’s own server documentation walks through, and it installs a system account called ftp with a home directory at /srv/ftp for anonymous access.
How do I check whether the FTP server is running on Ubuntu?
Run sudo systemctl is-active vsftpd, which prints active when the unit is up and the configuration file parsed. Then run sudo ss -tlnp and look for a listener on port 21. A unit that is active with no listener means it bound a different port or an address you did not expect.
Why does FTP connect and then fail to list a directory?
FTP uses two connections. The login completes over the control channel on port 21, and the directory listing needs a second connection to a passive port that vsftpd picks. If a firewall blocks that range, the login succeeds and every listing or transfer after it fails, which is what FileZilla reports as 425 Failed to establish connection. Fix the range with pasv_min_port and pasv_max_port, then open the same range in every firewall between the client and the server.
Is FTP safe to use over the internet?
Plain FTP sends the password and the file contents without encryption, so anyone on the path can read them. FTPS adds TLS to the same port 21 session, and SFTP moves the transfer into SSH on port 22, which is the better choice when the machine already runs an SSH server. Use plain FTP only on a trusted local network.
Which user does an anonymous FTP login use?
Anonymous logins use the ftp account, whose home directory is /srv/ftp on Ubuntu. Anonymous access is disabled in the shipped configuration, so the daemon refuses the login with 530 Login incorrect until anonymous_enable is set to YES. Uploads by anonymous users stay off unless anon_upload_enable is also enabled, and leaving them off is the safer default.
