Before using an FTP server
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.
| Package | Version | Notes |
|---|---|---|
vsftpd | 3.0.5 | In main. Used in this article |
proftpd-core | 1.3.9 | universe |
pure-ftpd | 1.0.50 | universe |
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.
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=0The ftp client command is installed by default (it is tnftp). curl speaks FTP and FTPS as well.
kazulog@sv2:~$ which ftp curl
/usr/bin/ftp
/usr/bin/curlInstall the FTP server
sudo apt install -y vsftpdssl-cert, which creates the certificate, is installed with it (used by FTPS later).
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 availableIt is enabled and started as part 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).
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.
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=NOThis 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.
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| Setting | Default | Line | Meaning |
|---|---|---|---|
anonymous_enable | NO | active line | Anonymous access (the ftp user) |
local_enable | YES | active line | Login as a system user |
write_enable | commented | 31 | All write commands. Without it you cannot upload |
local_umask | commented (= 077) | 35 | Permissions of uploaded files |
chroot_local_user | commented | 114 and 122 | Confine the user to their own directory |
ssl_enable | NO | 151 | FTPS |
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.
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.txtkazulog@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.txtLog 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.
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)
sudo vi /etc/vsftpd.confRemove the # on line 31.
write_enable=YESkazulog@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 vsftpdUploads now work, but the file is created as -rw------- (600), because the default is 077 when local_umask is absent.
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.
sudo vi /etc/vsftpd.conflocal_umask=022kazulog@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 vsftpdFiles uploaded after the change are -rw-r--r-- (644). The permissions of the file uploaded earlier do not 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.
sudo vi /etc/vsftpd.conf# Confine the user to their own directory
chroot_local_user=YES
user_sub_token=$USER
local_root=/srv/ftp-users/$USERCreate the published location. The top stays owned by root and only upload is owned by the user.
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.txtkazulog@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 vsftpdAfter 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.
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.
sudo vi /etc/vsftpd.confssl_enable=YESkazulog@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 vsftpdPlain-text logins are refused. tnftp does not support TLS, so the session is cut before the password is even asked for.
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> byeUse curl as the FTPS client. --ssl-reqd requires encryption and -k skips verification of the snakeoil certificate.
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=0Final 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.
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/$USERThe published location you created and the state of the service. The top is root:root and only upload is ftpdemo:ftpdemo.
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.
$ 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 ftpdemoLooking at the password packet in hex, the string is right there.
$ 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..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.
$ 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.
$ 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.
$ tshark -r ftp-server.pcap -n -Y 'frame contains "FtpIsPlaintext"' -T fields -e frame.number
9
80
136
192Looking at a packet after TLS started, there is no readable string.
$ 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.smIn the capture, AUTH SSL is answered with 234 Proceed with negotiation. and everything after that is TLS.
$ 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.
sudo vi /etc/vsftpd.confkazulog@sv1:~$ grep -n 'ssl_enable' /etc/vsftpd.conf
151:ssl_enable=NO
kazulog@sv1:~$ sudo systemctl restart vsftpdOn 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).
file prompt quiet
ip ftp username ftpdemo
ip ftp password FtpIsPlaintextThe 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.
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.
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 2026Once confirmed, revert the temporary settings on the router with no ip ftp username, no ip ftp password and no file prompt quiet.
Remove it
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.
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=0To 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.
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.
| Step | sv1 | sv2 | R1 |
|---|---|---|---|
| Initial state (right after wipe) | show / conf / log | show / conf / log | — |
| Installation (/etc/vsftpd.conf appears) | show / conf / log | — | — |
| Creating the test user | show / conf / log | — | — |
| Login and download (put gives 550) | — | show / conf / log / ftp | — |
| Line 31 write_enable | show / conf / log | ftp | — |
| Line 35 local_umask | show / conf / log | ftp | — |
| Appended (chroot and local_root) | show / conf / log | ftp | — |
| Line 151 ssl_enable (FTPS) | show / conf / log | show / conf / log / ftp | — |
| Final state of the configuration file | show / conf / log | — | — |
| FTPS off, saving from the router | show / conf / log | — | show |
| Removal (final state) | show / conf / log | — | — |
| Analysis of the capture | tshark | — | — |
References
Ubuntu Server: Set up an FTP server
RFC 959 - File Transfer Protocol
Related articles
- Changing the Hostname on Ubuntu 26.04 LTS Server (hostnamectl)
- Updating Packages on Ubuntu 26.04 LTS Server (apt update / upgrade)
- Setting the Timezone and Time Synchronisation on Ubuntu 26.04 LTS Server
- Creating Users and Granting sudo Privileges on Ubuntu 26.04 LTS Server
- Configuring the SSH Server on Ubuntu 26.04 LTS Server
- Setting Up a telnet Server on Ubuntu 26.04 LTS Server
- Configuring inetd (the Super-Server) on Ubuntu 26.04 LTS Server
- Setting Up a TFTP Server on Ubuntu 26.04 LTS Server (tftpd-hpa)
- FTP Server Setup on Ubuntu 26.04 LTS Server (vsftpd)
- Setting Up a syslog Server on Ubuntu 26.04 LTS Server (rsyslog)
- Managing Services with systemctl and Reading Logs with journalctl on Ubuntu 26.04 LTS Server
- Configuring Automatic Updates on Ubuntu 26.04 LTS Server (unattended-upgrades)
- Automating the Initial Setup of Ubuntu 26.04 LTS Server with cloud-init
- Configuring Kernel Parameters on Ubuntu 26.04 LTS Server (sysctl)
- Configuring the Network on Ubuntu 26.04 LTS Server (Netplan)
- Configuring Name Resolution on Ubuntu 26.04 LTS Server (systemd-resolved)
- Changing the NTP Source on Ubuntu 26.04 LTS Server (chrony)
- Static Routes on Ubuntu 26.04 LTS Server (Netplan)
- How to Install the Latest neovim from the Official Site on Ubuntu 26.04 LTS Server