I run my servers inside Proxmox, so in this case the FTP service lives on an Ubuntu Server VM there. The same steps apply whether you’re on bare metal or any other hypervisor.
This sets up plain FTP, which sends the username, the password and the file contents across the network unencrypted. On a home LAN behind a router, that’s a decision you can make for yourself, but not on anything exposed to the internet. The vsftpd.conf man page has an ssl_enable option that encrypts both the login and the data connections, or you can use SFTP over SSH instead of FTP.
Create the VM
Install Ubuntu Server LTS and give it a static LAN IP so it’s easy to reach later. I used a bridged adapter in Proxmox so the VM behaves like any other machine on my network.
If the VM can’t be reached from another machine, double-check the network adapter mode and make sure the IP address is in the right subnet.
Update the OS
Bring the system up to date before adding anything else.
sudo apt update && sudo apt upgrade -y
Mirror issues can cause failures. Running the command again usually clears it up.
Install vsftpd
Install the FTP daemon package.
sudo apt install vsftpd -y
If the service doesn’t appear to exist, confirm with systemctl status vsftpd.
Create an FTP User
I added a dedicated account just for FTP traffic so it’s separate from system logins.
sudo adduser ftpuser
Set a password when prompted.
Later login failures (530 Login incorrect) often mean the account is disabled in /etc/ftpusers or local_enable isn’t set in the config.
Set Up the Directory Structure
vsftpd refuses to start a session where the chroot directory itself is writable by the user. To work around this, I made a locked root folder with a writable subdirectory for uploads.
The man page doesn’t document that check, so the error message is the best guide to it. It does have a warning on chroot_local_user, which is the setting that jails users. It says the option “has security implications, especially if the users have upload permission, or shell access.” Keeping the root locked and the writable area down to one directory is one way to limit that.
sudo mkdir -p /srv/ftp/uploads
sudo chown root:root /srv/ftp
sudo chmod 755 /srv/ftp
sudo chown ftpuser:ftpuser /srv/ftp/uploads
Common errors:
500 OOPS: vsftpd: refusing to run with writable root inside chrootusually means permissions on/srv/ftpare wrong. It must be owned by root and not writable.
Configure vsftpd
Open the config:
sudo nano /etc/vsftpd.conf
Here’s what I used:
listen=YES
listen_ipv6=NO
anonymous_enable=NO
local_enable=YES
write_enable=YES
chroot_local_user=YES
That config sets up the account and the chroot but doesn’t pin the passive port range, so vsftpd picks from a wide default and the firewall rules further down won’t line up with it. The man page gives the default for both pasv_min_port and pasv_max_port as “0 (use any port),” and says a range “can be used to specify a narrow port range to assist firewalling,” which is the reason to set them at all. Add these two so the range matches what you open in UFW:
pasv_min_port=30000
pasv_max_port=30049
The man page says chroot_local_user=YES places local users “in a chroot() jail in their home directory after login.” adduser gives ftpuser a home directory under /home, so to use the /srv/ftp layout above, point the user at it. The local_root option sets the directory vsftpd changes into after a local login:
local_root=/srv/ftp
- If directory listings stall, the passive port range isn’t open.
- If the server advertises
127.0.1.1in passive mode, addpasv_address=<your_LAN_IP>to the config.
Restart and Enable the Service
Restart the daemon and enable it at boot.
sudo systemctl restart vsftpd
sudo systemctl enable vsftpd
If it fails to enable, check the logs:
journalctl -u vsftpd -n 50 --no-pager
Adjust the Firewall
If UFW is enabled, open the command port and the passive range.
sudo ufw allow 21/tcp
sudo ufw allow 30000:30049/tcp
UFW must be active for these rules to matter. Verify with sudo ufw status.
Test Locally
Before trying a client, I tested from the server itself.
curl -v --user 'ftpuser:YourPassword' ftp://127.0.0.1/
curl -v --user 'ftpuser:YourPassword' -T /etc/hosts ftp://127.0.0.1/uploads/
ls -l /srv/ftp/uploads
530 Login incorrect: check username, password, and config.550 Permission denied: verify/srv/ftp/uploadsis owned byftpuser.
Connect from Another Machine
Connect with an FTP client using your VM’s IP address.
- Host:
<LAN_IP> - Port: 21
- Protocol: FTP
- Username:
ftpuser - Password: your password
- Transfer mode: Passive
I only use these FTP servers on my local network, so I haven’t gone overboard with hardening them for internet exposure. Network security isn’t my specialty, so I stick to the basics. If you’ve got suggestions for tightening things up, either for FTP specifically or my home lab in general, I’d be glad to hear them.