Skip to main content
  1. Linux Articles/
  2. Ubuntu 26.04 LTS Server/

FTP Server Setup on Ubuntu 26.04 LTS Server (vsftpd)

Table of Contents

Before using an FTP server

FTP sends the user name, the password and the file contents without encryption. Do not use it on a server exposed to the Internet. To do the same thing with encryption, use SFTP / SCP on an SSH server, or the FTPS covered later in this article.

FTP (RFC 959) uses two TCP connections, one for control and one for data. The control connection is port 21 on the server, and commands and replies flow over it. File contents and directory listings travel over a separate connection that is opened again for each transfer. There are two ways to open the data connection: in passive mode the client connects to a port the server is listening on, and in active mode the server connects back to the client.

PackageVersionNotes
vsftpd3.0.5In main. Used in this article
proftpd-core1.3.9universe
pure-ftpd1.0.50universe

The only file edited in this article is /etc/vsftpd.conf. The published directory (/srv/ftp-users/) is one you create yourself.

Check what is installed

The server is not installed. There is no configuration file and nothing is listening on port 21.

Checking the server side
kazulog@sv1:~$ apt-cache policy vsftpd | head -3
vsftpd:
  Installed: (none)
  Candidate: 3.0.5-0.4
kazulog@sv1:~$ ls -l /etc/vsftpd.conf /srv/ftp 2>&1
ls: cannot access '/etc/vsftpd.conf': No such file or directory
ls: cannot access '/srv/ftp': No such file or directory
kazulog@sv1:~$ sudo ss -tlnp 'sport = :21'; echo "ss rc=$?"
State                  Recv-Q                  Send-Q                                   Local Address:Port                                   Peer Address:Port                 Process
ss rc=0

The ftp client command is installed by default (it is tnftp). curl speaks FTP and FTPS as well.

Checking the client side
kazulog@sv2:~$ which ftp curl
/usr/bin/ftp
/usr/bin/curl

Install the FTP server

Installation command
sudo apt install -y vsftpd

ssl-cert, which creates the certificate, is installed with it (used by FTPS later).

Packages installed
Installing:
  vsftpd

Installing dependencies:
  ssl-cert

Summary:
  Upgrading: 0, Installing: 2, Removing: 0, Not Upgrading: 96
  Download size: 141 kB
  Space needed: 385 kB / 63.1 GB available

It is enabled and started as part of the installation.

End of the installation
Setting up ssl-cert (1.1.3ubuntu2) ...
Created symlink '/etc/systemd/system/multi-user.target.wants/ssl-cert.service''/usr/lib/systemd/system/ssl-cert.service'.
Setting up vsftpd (3.0.5-0.4) ...
Created symlink '/etc/systemd/system/multi-user.target.wants/vsftpd.service''/usr/lib/systemd/system/vsftpd.service'.
/usr/lib/tmpfiles.d/vsftpd.conf:1: Line references path below legacy directory /var/run/, updating /var/run/vsftpd/empty → /run/vsftpd/empty; please update the tmpfiles.d/ drop-in file accordingly.

It listens on port 21. The display shows *:21 because the IPv6 socket also accepts IPv4 (listen=NO and listen_ipv6=YES).

State right after installation
kazulog@sv1:~$ systemctl status vsftpd --no-pager | head -6 | cat
● vsftpd.service - vsftpd FTP server
     Loaded: loaded (/usr/lib/systemd/system/vsftpd.service; enabled; preset: enabled)
     Active: active (running) since Sat 2026-09-12 10:16:30 JST; 7s ago
 Invocation: 6e752ef8a1d14ad8895116d39f89c1bf
    Process: 2759 ExecStartPre=/bin/mkdir -p /var/run/vsftpd/empty (code=exited, status=0/SUCCESS)
   Main PID: 2761 (vsftpd)
kazulog@sv1:~$ sudo ss -tlnp 'sport = :21'
State               Recv-Q              Send-Q                             Local Address:Port                             Peer Address:Port              Process
LISTEN              0                   32                                             *:21                                          *:*                  users:(("vsftpd",pid=2761,fd=3))              

The configuration file (/etc/vsftpd.conf)

The configuration is the single file /etc/vsftpd.conf. Excluding comments it has 13 active lines right after installation, with anonymous access disabled and system users allowed to log in.

Active lines only
kazulog@sv1:~$ grep -vE '^(#|$)' /etc/vsftpd.conf
listen=NO
listen_ipv6=YES
anonymous_enable=NO
local_enable=YES
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
ssl_enable=NO

This article touches four places. write_enable, local_umask and chroot_local_user are present but disabled with #, so you find that line and either remove the # or change the value.

Where the lines to edit are
kazulog@sv1:~$ grep -n 'write_enable\|local_umask\|chroot_local_user\|ssl_enable' /etc/vsftpd.conf
31:#write_enable=YES
35:#local_umask=022
44:#anon_mkdir_write_enable=YES
112:# the possible risks in this before using chroot_local_user or
114:#chroot_local_user=YES
117:# directory. If chroot_local_user is YES, then this list becomes a list of
122:#chroot_local_user=YES
151:ssl_enable=NO
SettingDefaultLineMeaning
anonymous_enableNOactive lineAnonymous access (the ftp user)
local_enableYESactive lineLogin as a system user
write_enablecommented31All write commands. Without it you cannot upload
local_umaskcommented (= 077)35Permissions of uploaded files
chroot_local_usercommented114 and 122Confine the user to their own directory
ssl_enableNO151FTPS

The commented chroot_local_user appears twice, on lines 114 and 122. Removing the # from either one has the same effect, but this article appends to the end of the file instead, so that the three related lines sit together.

After a change, sudo systemctl restart vsftpd applies it. Users listed in /etc/ftpusers (such as root) cannot log in over FTP.

Log in and download a file

Create a user for the test. This password is a test-only string used to show that it travels in the clear; do not use it in production.

Commands to create the test user
sudo useradd -m -s /bin/bash ftpdemo
echo 'ftpdemo:FtpIsPlaintext' | sudo chpasswd
echo "hello from ftp server sv1" | sudo -u ftpdemo tee /home/ftpdemo/hello.txt
Creating the test user
kazulog@sv1:~$ sudo useradd -m -s /bin/bash ftpdemo
kazulog@sv1:~$ echo 'ftpdemo:FtpIsPlaintext' | sudo chpasswd
kazulog@sv1:~$ id ftpdemo
uid=1001(ftpdemo) gid=1001(ftpdemo) groups=1001(ftpdemo)
kazulog@sv1:~$ echo "hello from ftp server sv1" | sudo -u ftpdemo tee /home/ftpdemo/hello.txt

Log in with ftp from another server. The current directory is the home directory /home/ftpdemo, get succeeds, and put fails with 550 Permission denied. because write_enable is absent.

Logging in and downloading
kazulog@sv2:~$ cd /tmp
kazulog@sv2:/tmp$ echo "uploaded from sv2" > upload.txt
kazulog@sv2:/tmp$ ftp 192.168.100.10
Connected to 192.168.100.10.
220 (vsFTPd 3.0.5)
Name (192.168.100.10:kazulog): ftpdemo
331 Please specify the password.
Password: 
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> pwd
Remote directory: /home/ftpdemo
ftp> ls
229 Entering Extended Passive Mode (|||59134|)
150 Here comes the directory listing.
-rw-r--r--    1 1001     1001           26 Sep 12 10:16 hello.txt
226 Directory send OK.
ftp> get hello.txt
local: hello.txt remote: hello.txt
229 Entering Extended Passive Mode (|||53233|)
150 Opening BINARY mode data connection for hello.txt (26 bytes).
100% |***********************************************************************************************************************************************************|    26       60.88 KiB/s    00:00 ETA
226 Transfer complete.
26 bytes received in 00:00 (9.26 KiB/s)
ftp> put upload.txt
local: upload.txt remote: upload.txt
229 Entering Extended Passive Mode (|||47977|)
550 Permission denied.
ftp> bye
221 Goodbye.

229 Entering Extended Passive Mode is the passive-mode reply, and the number in the parentheses is the port of the data connection.

Allow uploads (line 31)

File to edit
sudo vi /etc/vsftpd.conf

Remove the # on line 31.

Line 31 of /etc/vsftpd.conf (after the change)
write_enable=YES
Checking the change
kazulog@sv1:~$ grep -n 'write_enable\|local_umask' /etc/vsftpd.conf
31:write_enable=YES
35:#local_umask=022
44:#anon_mkdir_write_enable=YES
kazulog@sv1:~$ sudo systemctl restart vsftpd

Uploads now work, but the file is created as -rw------- (600), because the default is 077 when local_umask is absent.

Uploading
kazulog@sv2:~$ cd /tmp
kazulog@sv2:/tmp$ ftp 192.168.100.10
Connected to 192.168.100.10.
220 (vsFTPd 3.0.5)
Name (192.168.100.10:kazulog): ftpdemo
331 Please specify the password.
Password: 
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> put upload.txt
local: upload.txt remote: upload.txt
229 Entering Extended Passive Mode (|||52014|)
150 Ok to send data.
100% |***********************************************************************************************************************************************************|    18       29.15 KiB/s    00:00 ETA
226 Transfer complete.
18 bytes sent in 00:00 (3.69 KiB/s)
ftp> ls
229 Entering Extended Passive Mode (|||53689|)
150 Here comes the directory listing.
-rw-r--r--    1 1001     1001           26 Sep 12 10:16 hello.txt
-rw-------    1 1001     1001           18 Sep 12 10:20 upload.txt
226 Directory send OK.
ftp> bye
221 Goodbye.

Decide the permissions of uploaded files (line 35)

To let other users read them, remove the # on line 35.

File to edit
sudo vi /etc/vsftpd.conf
Line 35 of /etc/vsftpd.conf (after the change)
local_umask=022
Checking the change
kazulog@sv1:~$ sudo ls -l /home/ftpdemo/
total 8
-rw-r--r-- 1 ftpdemo ftpdemo 26 Sep 12 10:16 hello.txt
-rw------- 1 ftpdemo ftpdemo 18 Sep 12 10:20 upload.txt
kazulog@sv1:~$ grep -n 'write_enable\|local_umask' /etc/vsftpd.conf
31:write_enable=YES
35:local_umask=022
44:#anon_mkdir_write_enable=YES
kazulog@sv1:~$ sudo systemctl restart vsftpd

Files uploaded after the change are -rw-r--r-- (644). The permissions of the file uploaded earlier do not change.

Uploading after the change
kazulog@sv2:~$ cd /tmp
kazulog@sv2:/tmp$ ftp 192.168.100.10
Connected to 192.168.100.10.
220 (vsFTPd 3.0.5)
Name (192.168.100.10:kazulog): ftpdemo
331 Please specify the password.
Password: 
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> put upload.txt upload2.txt
local: upload.txt remote: upload2.txt
229 Entering Extended Passive Mode (|||6067|)
150 Ok to send data.
100% |***********************************************************************************************************************************************************|    18       43.94 KiB/s    00:00 ETA
226 Transfer complete.
18 bytes sent in 00:00 (4.41 KiB/s)
ftp> ls
229 Entering Extended Passive Mode (|||21507|)
150 Here comes the directory listing.
-rw-r--r--    1 1001     1001           26 Sep 12 10:16 hello.txt
-rw-------    1 1001     1001           18 Sep 12 10:20 upload.txt
-rw-r--r--    1 1001     1001           18 Sep 12 10:21 upload2.txt
226 Directory send OK.
ftp> bye
221 Goodbye.

Confine the user to their own directory (append to the end)

With chroot_local_user=YES, a user who logs in cannot go above the published directory. However, vsftpd refuses the login if the top directory of the confinement is writable by that user (500 OOPS: vsftpd: refusing to run with writable root inside chroot()). Using the home directory as-is makes it writable by the user, so the usual approach is to keep the top directory owned by root and put a writable directory underneath it.

local_root changes the published location and user_sub_token separates it per user.

File to edit
sudo vi /etc/vsftpd.conf
Appended to the end of /etc/vsftpd.conf
# Confine the user to their own directory
chroot_local_user=YES
user_sub_token=$USER
local_root=/srv/ftp-users/$USER

Create the published location. The top stays owned by root and only upload is owned by the user.

Commands to create the published location
sudo mkdir -p /srv/ftp-users/ftpdemo/upload
sudo chown ftpdemo:ftpdemo /srv/ftp-users/ftpdemo/upload
echo "hello from ftp server sv1" | sudo tee /srv/ftp-users/ftpdemo/hello.txt
Checking the change
kazulog@sv1:~$ tail -5 /etc/vsftpd.conf
#utf8_filesystem=YES
# 利用者を自分のディレクトリに閉じ込める
chroot_local_user=YES
user_sub_token=$USER
local_root=/srv/ftp-users/$USER
kazulog@sv1:~$ sudo ls -laR /srv/ftp-users/
/srv/ftp-users/:
total 12
drwxr-xr-x 3 root root 4096 Sep 12 10:22 .
drwxr-xr-x 4 root root 4096 Sep 12 10:22 ..
drwxr-xr-x 3 root root 4096 Sep 12 10:22 ftpdemo

/srv/ftp-users/ftpdemo:
total 16
drwxr-xr-x 3 root    root    4096 Sep 12 10:22 .
drwxr-xr-x 3 root    root    4096 Sep 12 10:22 ..
-rw-r--r-- 1 root    root      26 Sep 12 10:22 hello.txt
drwxr-xr-x 2 ftpdemo ftpdemo 4096 Sep 12 10:22 upload

/srv/ftp-users/ftpdemo/upload:
total 8
drwxr-xr-x 2 ftpdemo ftpdemo 4096 Sep 12 10:22 .
drwxr-xr-x 3 root    root    4096 Sep 12 10:22 ..
kazulog@sv1:~$ sudo systemctl restart vsftpd

After logging in the current directory is / and there is no way above it. Uploading to the top fails (553), while the writable directory accepts it.

Checking the confinement
kazulog@sv2:~$ cd /tmp
kazulog@sv2:/tmp$ ftp 192.168.100.10
Connected to 192.168.100.10.
220 (vsFTPd 3.0.5)
Name (192.168.100.10:kazulog): ftpdemo
331 Please specify the password.
Password: 
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> pwd
Remote directory: /
ftp> ls
229 Entering Extended Passive Mode (|||47435|)
150 Here comes the directory listing.
-rw-r--r--    1 0        0              26 Sep 12 10:22 hello.txt
drwxr-xr-x    2 1001     1001         4096 Sep 12 10:22 upload
226 Directory send OK.
ftp> cd /home
550 Failed to change directory.
ftp> put upload.txt
local: upload.txt remote: upload.txt
229 Entering Extended Passive Mode (|||51762|)
553 Could not create file.
ftp> cd upload
250 Directory successfully changed.
ftp> put upload.txt
local: upload.txt remote: upload.txt
229 Entering Extended Passive Mode (|||31305|)
150 Ok to send data.
100% |***********************************************************************************************************************************************************|    18       36.02 KiB/s    00:00 ETA
226 Transfer complete.
18 bytes sent in 00:00 (3.83 KiB/s)
ftp> ls
229 Entering Extended Passive Mode (|||14734|)
150 Here comes the directory listing.
-rw-r--r--    1 1001     1001           18 Sep 12 10:23 upload.txt
226 Directory send OK.
ftp> pwd
Remote directory: /upload
ftp> bye
221 Goodbye.

Encrypt the session (FTPS, line 151)

Setting line 151 to YES turns it into FTPS (FTP over TLS, RFC 4217). The certificate is the snakeoil one installed with the package, already referenced on lines 149 and 150.

File to edit
sudo vi /etc/vsftpd.conf
Line 151 of /etc/vsftpd.conf (after the change)
ssl_enable=YES
Checking the change
kazulog@sv1:~$ grep -n 'ssl_enable\|rsa_cert_file\|rsa_private_key_file' /etc/vsftpd.conf
149:rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
150:rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
151:ssl_enable=YES
kazulog@sv1:~$ sudo systemctl restart vsftpd

Plain-text logins are refused. tnftp does not support TLS, so the session is cut before the password is even asked for.

A plain-text login being refused
kazulog@sv2:~$ cd /tmp
kazulog@sv2:/tmp$ ftp 192.168.100.10
Connected to 192.168.100.10.
220 (vsFTPd 3.0.5)
Name (192.168.100.10:kazulog): ftpdemo
530 Non-anonymous sessions must use encryption.
ftp: Login failed
ftp> bye

Use curl as the FTPS client. --ssl-reqd requires encryption and -k skips verification of the snakeoil certificate.

FTPS with curl
kazulog@sv2:~$ curl -sS -u ftpdemo:FtpIsPlaintext ftp://192.168.100.10/hello.txt; echo "rc=$?"
curl: (67) Access denied: 530
rc=67
kazulog@sv2:~$ curl -sS --ssl-reqd -k -u ftpdemo:FtpIsPlaintext ftp://192.168.100.10/hello.txt; echo "rc=$?"
hello from ftp server sv1
rc=0
kazulog@sv2:~$ curl -sS --ssl-reqd -k -T /tmp/upload.txt -u ftpdemo:FtpIsPlaintext ftp://192.168.100.10/upload/curl.txt; echo "rc=$?"
rc=0

Final state of the configuration file

This is the state with every setting above applied. The only file touched is /etc/vsftpd.conf, whose 13 active lines after installation have become 18, with 5 lines added.

Active lines of /etc/vsftpd.conf
kazulog@sv1:~$ grep -vE '^(#|$)' /etc/vsftpd.conf
listen=NO
listen_ipv6=YES
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=022
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
ssl_enable=YES
chroot_local_user=YES
user_sub_token=$USER
local_root=/srv/ftp-users/$USER

The published location you created and the state of the service. The top is root:root and only upload is ftpdemo:ftpdemo.

Published location and service state
kazulog@sv1:~$ sudo ls -laR /srv/ftp-users/
/srv/ftp-users/:
total 12
drwxr-xr-x 3 root root 4096 Sep 12 10:22 .
drwxr-xr-x 4 root root 4096 Sep 12 10:22 ..
drwxr-xr-x 3 root root 4096 Sep 12 10:22 ftpdemo

/srv/ftp-users/ftpdemo:
total 16
drwxr-xr-x 3 root    root    4096 Sep 12 10:22 .
drwxr-xr-x 3 root    root    4096 Sep 12 10:22 ..
-rw-r--r-- 1 root    root      26 Sep 12 10:22 hello.txt
drwxr-xr-x 2 ftpdemo ftpdemo 4096 Sep 12 10:24 upload

/srv/ftp-users/ftpdemo/upload:
total 16
drwxr-xr-x 2 ftpdemo ftpdemo 4096 Sep 12 10:24 .
drwxr-xr-x 3 root    root    4096 Sep 12 10:22 ..
-rw-r--r-- 1 ftpdemo ftpdemo   18 Sep 12 10:24 curl.txt
-rw-r--r-- 1 ftpdemo ftpdemo   18 Sep 12 10:23 upload.txt
kazulog@sv1:~$ systemctl is-enabled vsftpd; systemctl is-active vsftpd
enabled
active
kazulog@sv1:~$ sudo ss -tlnp 'sport = :21'
State               Recv-Q              Send-Q                             Local Address:Port                             Peer Address:Port              Process
LISTEN              0                   32                                             *:21                                          *:*                  users:(("vsftpd",pid=5460,fd=3))              

Confirm that it travels in the clear

Capturing on the link between sv1 and the switch shows the user name and the password in plain text. The last two (No.277 and No.292) are after FTPS was enabled, so they are cut at USER and no PASS follows.

USER and PASS
$ tshark -r ftp-server.pcap -n -Y 'ftp.request.command == "USER" || ftp.request.command == "PASS"' -T fields -e frame.number -e ip.src -e ftp.request.command -e ftp.request.arg
6	192.168.100.20	USER	ftpdemo
9	192.168.100.20	PASS	FtpIsPlaintext
77	192.168.100.20	USER	ftpdemo
80	192.168.100.20	PASS	FtpIsPlaintext
133	192.168.100.20	USER	ftpdemo
136	192.168.100.20	PASS	FtpIsPlaintext
189	192.168.100.20	USER	ftpdemo
192	192.168.100.20	PASS	FtpIsPlaintext
277	192.168.100.20	USER	ftpdemo
292	192.168.100.20	USER	ftpdemo

Looking at the password packet in hex, the string is right there.

Contents of No.9
$ tshark -r ubuntu-ftp-no9.pcap -n -x
0000  52 54 00 30 7e c3 52 54 00 78 56 37 08 00 45 10   RT.0~.RT.xV7..E.
0010  00 49 d0 db 40 00 40 06 20 54 c0 a8 64 14 c0 a8   .I..@.@. T..d...
0020  64 0a b8 84 00 15 af 9b d2 31 5a d4 22 ec 80 18   d........1Z."...
0030  00 fb e7 1e 00 00 01 01 08 0a 2d 4d d3 94 eb a6   ..........-M....
0040  1a d8 50 41 53 53 20 46 74 70 49 73 50 6c 61 69   ..PASS FtpIsPlai
0050  6e 74 65 78 74 0d 0a                              ntext..
Download the pcap of the packet in the tshark output above (No.9 PASS)

The data connection being a separate TCP connection is visible too. The client connects to port 53233 given in the 229 reply (No.42-44), the file contents flow over it, and it closes.

The data connection
$ tshark -r ftp-server.pcap -n -Y 'frame.number >= 40 && frame.number <= 52' -T fields -e frame.number -e ip.src -e tcp.srcport -e ip.dst -e tcp.dstport -e _ws.col.info
40	192.168.100.20	47236	192.168.100.10	21	Request: EPSV
41	192.168.100.10	21	192.168.100.20	47236	Response: 229 Entering Extended Passive Mode (|||53233|)
42	192.168.100.20	60442	192.168.100.10	53233	60442 → 53233 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=2184570297 TSecr=0 WS=256
43	192.168.100.10	53233	192.168.100.20	60442	53233 → 60442 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=2456829945 TSecr=2184570297 WS=256
44	192.168.100.20	60442	192.168.100.10	53233	60442 → 53233 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=2184570299 TSecr=2456829945
45	192.168.100.20	47236	192.168.100.10	21	Request: RETR hello.txt
46	192.168.100.10	21	192.168.100.20	47236	Response: 150 Opening BINARY mode data connection for hello.txt (26 bytes).
47	192.168.100.10	53233	192.168.100.20	60442	FTP Data: 26 bytes (EPASV) (RETR hello.txt)
48	192.168.100.10	53233	192.168.100.20	60442	53233 → 60442 [FIN, ACK] Seq=27 Ack=1 Win=65280 Len=0 TSval=2456829947 TSecr=2184570299
49	192.168.100.20	60442	192.168.100.10	53233	60442 → 53233 [ACK] Seq=1 Ack=27 Win=64256 Len=0 TSval=2184570301 TSecr=2456829947
50	192.168.100.20	60442	192.168.100.10	53233	60442 → 53233 [FIN, ACK] Seq=1 Ack=28 Win=64256 Len=0 TSval=2184570302 TSecr=2456829947
51	192.168.100.10	53233	192.168.100.20	60442	53233 → 60442 [ACK] Seq=28 Ack=2 Win=65280 Len=0 TSval=2456829950 TSecr=2184570302
52	192.168.100.10	21	192.168.100.20	47236	Response: 226 Transfer complete.

The passive-mode port changes every time. To pin the range down, specify pasv_min_port and pasv_max_port.

Passive-mode ports
$ tshark -r ftp-server.pcap -n -Y 'ftp.response.code == 229' -T fields -e frame.number -e ftp.response.arg
23	Entering Extended Passive Mode (|||59134|)
41	Entering Extended Passive Mode (|||53233|)
57	Entering Extended Passive Mode (|||47977|)
94	Entering Extended Passive Mode (|||52014|)
110	Entering Extended Passive Mode (|||53689|)
150	Entering Extended Passive Mode (|||6067|)
166	Entering Extended Passive Mode (|||21507|)
206	Entering Extended Passive Mode (|||47435|)
224	Entering Extended Passive Mode (|||51762|)
236	Entering Extended Passive Mode (|||31305|)
252	Entering Extended Passive Mode (|||14734|)

Only the four plain-text logins contain the password; the FTPS login does not.

Frames containing the password
$ tshark -r ftp-server.pcap -n -Y 'frame contains "FtpIsPlaintext"' -T fields -e frame.number
9
80
136
192

Looking at a packet after TLS started, there is no readable string.

Contents of No.346
$ tshark -r ubuntu-ftp-no346.pcap -n -x
0000  52 54 00 30 7e c3 52 54 00 78 56 37 08 00 45 00   RT.0~.RT.xV7..E.
0010  00 7e d6 cf 40 00 40 06 1a 3b c0 a8 64 14 c0 a8   .~..@.@..;..d...
0020  64 0a ab d0 5a c3 e3 d3 c3 78 be 6f 14 d9 80 18   d...Z....x.o....
0030  00 fb 1f fb 00 00 01 01 08 0a db 9a d4 4d 67 1f   .............Mg.
0040  1a ce 17 03 03 00 45 7d 26 01 1c 7d 16 17 0b 95   ......E}&..}....
0050  1f 8b de fa fa e4 77 8e 2c 45 83 69 76 18 ef 4f   ......w.,E.iv..O
0060  b5 d3 ac 11 c2 71 ec 56 a2 a1 c5 06 ce 27 d8 a8   .....q.V.....'..
0070  8a b9 19 27 2d b6 28 27 dc c9 1f ad d0 28 c6 10   ...'-.('.....(..
0080  37 c1 ef 9d 08 14 90 d4 30 00 73 6d               7.......0.sm
Download the pcap of the packet in the tshark output above (No.346 Application Data)

In the capture, AUTH SSL is answered with 234 Proceed with negotiation. and everything after that is TLS.

AUTH and the reply
$ tshark -r ftp-server.pcap -n -Y 'ftp.request.command == "AUTH" || ftp.response.code == 234' -T fields -e frame.number -e ip.src -e ftp.request.arg -e ftp.response.arg
303	192.168.100.20	SSL	
305	192.168.100.10		Proceed with negotiation.
369	192.168.100.20	SSL	
371	192.168.100.10		Proceed with negotiation.

Save a router configuration

Upload the running-config from the IOS XE router R1 with the copy command. The router only speaks plain FTP, so line 151 goes back to ssl_enable=NO first.

File to edit
sudo vi /etc/vsftpd.conf
Turning FTPS back off
kazulog@sv1:~$ grep -n 'ssl_enable' /etc/vsftpd.conf
151:ssl_enable=NO
kazulog@sv1:~$ sudo systemctl restart vsftpd

On the router, set the user name and the password with ip ftp username / ip ftp password (file prompt quiet is there to skip the confirmation prompt).

Configuration commands on R1
file prompt quiet
ip ftp username ftpdemo
ip ftp password FtpIsPlaintext

The destination URL follows how the tree looks after the confinement. With local_root=/srv/ftp-users/$USER, the top as seen from the router is /srv/ftp-users/ftpdemo, and only upload/ is writable.

Saving the configuration from R1
R1#copy running-config ftp://192.168.100.10/upload/r1-confg
Writing upload/r1-confg !
4977 bytes copied in 3.419 secs (1456 bytes/sec)

It has arrived on the server.

On the server
kazulog@sv1:~$ sudo ls -l /srv/ftp-users/ftpdemo/upload/
total 8
-rw-r--r-- 1 ftpdemo ftpdemo 18 Sep 12 10:24 curl.txt
-rw-r--r-- 1 ftpdemo ftpdemo 18 Sep 12 10:23 upload.txt
kazulog@sv1:~$ sudo ls -l /srv/ftp-users/ftpdemo/upload/
total 16
-rw-r--r-- 1 ftpdemo ftpdemo   18 Sep 12 10:24 curl.txt
-rw-r--r-- 1 ftpdemo ftpdemo 4977 Sep 12 10:30 r1-confg
-rw-r--r-- 1 ftpdemo ftpdemo   18 Sep 12 10:23 upload.txt
kazulog@sv1:~$ sudo head -3 /srv/ftp-users/ftpdemo/upload/r1-confg
!
! Last configuration change at 01:29:39 UTC Sat Sep 12 2026

Once confirmed, revert the temporary settings on the router with no ip ftp username, no ip ftp password and no file prompt quiet.

Remove it

Removal command
sudo apt purge -y vsftpd

/etc/vsftpd.conf is deleted, but the ftp user and the /srv/ftp-users/ you created (including the uploaded files) remain. ssl-cert, installed as a dependency, also remains.

Removal
kazulog@sv1:~$ sudo apt purge -y vsftpd | cat
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

Reading package lists...
Building dependency tree...
Reading state information...
Solving dependencies...
The following package was automatically installed and is no longer required:
  ssl-cert
Use 'sudo apt autoremove' to remove it.

REMOVING:
  vsftpd*

Summary:
  Upgrading: 0, Installing: 0, Removing: 1, Not Upgrading: 96
  Freed space: 316 kB

(Reading database ... 91464 files and directories currently installed.)
Removing vsftpd (3.0.5-0.4) ...
Processing triggers for man-db (2.13.1-1build1) ...
(Reading database ... 91413 files and directories currently installed.)
Purging configuration files for vsftpd (3.0.5-0.4) ...
kazulog@sv1:~$ dpkg -l vsftpd 2>&1 | tail -1
dpkg-query: no packages found matching vsftpd
kazulog@sv1:~$ ls -l /etc/vsftpd.conf 2>&1
ls: cannot access '/etc/vsftpd.conf': No such file or directory
kazulog@sv1:~$ sudo ls -ld /srv/ftp /srv/ftp-users/ftpdemo/upload 2>&1
ls: cannot access '/srv/ftp': No such file or directory
drwxr-xr-x 2 ftpdemo ftpdemo 4096 Sep 12 10:30 /srv/ftp-users/ftpdemo/upload
kazulog@sv1:~$ getent passwd ftp
ftp:x:103:110:ftp daemon:/srv/ftp:/usr/sbin/nologin
kazulog@sv1:~$ sudo ss -tlnp 'sport = :21'; echo "ss rc=$?"
State                  Recv-Q                  Send-Q                                   Local Address:Port                                   Peer Address:Port                 Process
ss rc=0

To remove the published location as well, run sudo userdel -r ftpdemo and sudo rm -rf /srv/ftp-users separately.

Test environment and logs

The data was captured on CML with two Ubuntu 26.04 LTS Servers and one IOS XE router. The Ubuntu disks start from the state right after a wipe, because a procedure written on top of packages left over from an earlier test does not reproduce.

Test topology
  sv1                         sv2                         R1
  ens3 192.168.100.10/24      ens3 192.168.100.20/24      Gi2 192.168.100.1/24
  vsftpd (TCP 21)             ftp / curl                  IOS XE 17.03.08a
   |                           |                           |
   +--------- LAB-SW ----------+---------------------------+
   ^ captured on this link (TCP with sv2 only)

The whole capture file.

Capture of FTP and FTPS (ubuntu-ftp.pcap)

..._ftp.txt is the transcript of the ftp command on sv2.

Stepsv1sv2R1
Initial state (right after wipe)show / conf / logshow / conf / log
Installation (/etc/vsftpd.conf appears)show / conf / log
Creating the test usershow / conf / log
Login and download (put gives 550)show / conf / log / ftp
Line 31 write_enableshow / conf / logftp
Line 35 local_umaskshow / conf / logftp
Appended (chroot and local_root)show / conf / logftp
Line 151 ssl_enable (FTPS)show / conf / logshow / conf / log / ftp
Final state of the configuration fileshow / conf / log
FTPS off, saving from the routershow / conf / logshow
Removal (final state)show / conf / log
Analysis of the capturetshark

References

Ubuntu Server: Set up an FTP server

RFC 959 - File Transfer Protocol

Related articles

Ubuntu official pages