Updated: 12 October 2026 · Applies to: vsftpd 3.0.5 on AlmaLinux 10.2 and 9.8 (both tested); Rocky Linux and RHEL 9 and 10 use the same Red Hat package
To set up an FTP server on Red Hat Enterprise Linux, AlmaLinux or Rocky Linux, install vsftpd with dnf install vsftpd, require TLS, lock each FTP user into their own folder, allow only the users you list, and open port 21 plus a fixed passive port range in firewalld. SELinux can stay in enforcing mode: this setup needs no SELinux boolean. CentOS 7 is end of life; the steps below are tested on AlmaLinux 10.2 and 9.8. For Ubuntu, see how to set up an FTP server on Ubuntu with vsftpd.
Only moving files yourself? SFTP works over the SSH server you already have and needs no FTP server: see how to use SFTP. Use FTP when a person, a device or a program needs FTP itself.
1. Install vsftpd
dnf install vsftpd cp /etc/vsftpd/vsftpd.conf /etc/vsftpd/vsftpd.conf.orig
The package’s configuration already blocks anonymous logins and allows local users to log in and upload. TLS, the folder lock and the passive ports are added in step 4.
2. Create the FTP user and its folder
useradd -m ftpuser passwd ftpuser mkdir -p /home/ftpuser/ftp/files chown ftpuser:ftpuser /home/ftpuser/ftp/files chmod 755 /home/ftpuser/ftp echo ftpuser >> /etc/vsftpd/allowed_users
The user is locked into /home/ftpuser/ftp. That folder belongs to root and is not writable for the user, because vsftpd refuses a writable top folder; uploads go into files. Only users listed in /etc/vsftpd/allowed_users may log in (step 4).
FTP-only user without SSH login: create the user with useradd -m -s /usr/sbin/nologin ftpuser. The PAM file /etc/pam.d/vsftpd uses pam_shells, which only accepts shells listed in /etc/shells, so such a user gets 530 Login incorrect until you run echo /usr/sbin/nologin >> /etc/shells. That made the login work in our test. Other programs that check /etc/shells then also accept nologin as a shell.
root and the system accounts in /etc/vsftpd/ftpusers are always refused: even with root in the allowed list, our test got 530 Login incorrect.
3. Get a TLS certificate
- Let’s Encrypt (recommended): point a name such as
ftp.example.comto the server. Certbot comes from the EPEL repository: on AlmaLinux rundnf install epel-release, thendnf install certbot(certbot 4.2.0 on AlmaLinux 10.2 in our test); on RHEL, enable EPEL first as its documentation describes. Runcertbot certonly --standalone -d ftp.example.com --deploy-hook "systemctl restart vsftpd". According to the Certbot documentation, the standalone mode needs port 80 free and reachable (open it withfirewall-cmd --permanent --add-service=http), the files are saved as/etc/letsencrypt/live/ftp.example.com/fullchain.pemandprivkey.pem, and the deploy hook restarts vsftpd after each renewal. - Self-signed, for testing: FTP clients show a warning for it.
openssl req -x509 -noenc -days 825 -newkey rsa:2048 -subj "/CN=ftp.example.com" \ -keyout /etc/pki/tls/private/vsftpd.key -out /etc/pki/tls/certs/vsftpd.crt chmod 600 /etc/pki/tls/private/vsftpd.key
4. Configure /etc/vsftpd/vsftpd.conf
Replace the file’s contents with this (the original is saved as vsftpd.conf.orig). The first block repeats the package defaults; with Let’s Encrypt, put its two paths in the rsa_ lines.
anonymous_enable=NO local_enable=YES write_enable=YES local_umask=022 dirmessage_enable=YES xferlog_enable=YES xferlog_std_format=YES connect_from_port_20=YES listen=NO listen_ipv6=YES pam_service_name=vsftpd chroot_local_user=YES user_sub_token=$USER local_root=/home/$USER/ftp userlist_enable=YES userlist_file=/etc/vsftpd/allowed_users userlist_deny=NO pasv_min_port=40000 pasv_max_port=40100 ssl_enable=YES rsa_cert_file=/etc/pki/tls/certs/vsftpd.crt rsa_private_key_file=/etc/pki/tls/private/vsftpd.key force_local_logins_ssl=YES force_local_data_ssl=YES
listen=NOwithlisten_ipv6=YESis the package default and serves IPv4 and IPv6.chroot_local_user,user_sub_tokenandlocal_rootlock every user into/home/USERNAME/ftp.userlist_deny=NOturns the user list into an allow list: only users in/etc/vsftpd/allowed_usersmay log in. The package’s own/etc/vsftpd/user_listis a deny list of system accounts and is no longer used. More in allow or deny users on vsftpd.pasv_min_portandpasv_max_portfix the passive data ports so you can open them in the firewall. Behind NAT, also setpasv_address=to the public IP. Background: FTP active vs passive mode.- The last five lines require TLS for the password and for all transfers. You need no TLS version options: our servers accepted TLS 1.3 and TLS 1.2. Unlike Ubuntu’s build, this vsftpd knows
ssl_tlsv1_2and started with it in our tests, so the line from older guides does no harm here, but it is not needed.
5. Start vsftpd
systemctl enable --now vsftpd systemctl status vsftpd
A typo in the file stops it with a message such as 500 OOPS: unrecognised variable in config file: ssl_tlsv12. See vsftpd will not start.
6. Open the firewall (firewalld)
firewall-cmd --permanent --add-service=ftp firewall-cmd --permanent --add-port=40000-40100/tcp firewall-cmd --reload
The ftp service opens port 21/tcp and loads firewalld’s FTP helper, which opens data ports by reading the FTP commands. With TLS those commands are encrypted, so the helper cannot see them and the passive range must be opened by hand. Port 20 needs no rule. More commands: firewall-cmd examples.
7. SELinux: leave it enforcing
We checked the SELinux policy of AlmaLinux 10.2 (selinux-policy 42.1.18) and 9.8 (selinux-policy 38.1.75) with sesearch. In both, vsftpd (domain ftpd_t) may already read and write files in users’ home folders (type user_home_t) and may use ports 32768 to 60999 (type ephemeral_port_t, which includes 40000 to 40100) without any boolean. So this setup needs no setsebool command.
- The policy’s default label for
/home/ftpuser/ftp/filesisuser_home_t(checked withmatchpathcon). If you moved files in from elsewhere, they may keep their old label: reset it withrestorecon -Rv /home/ftpuserand check withls -Z. - Only if FTP users must write outside
/home, for example into a website folder in/var/www, turn onsetsebool -P ftpd_full_access on. It lets vsftpd reach every file that normal permissions allow, so use it only when needed. If an upload into a home folder is still refused while SELinux is enforcing, check for a denial withausearch -m avc -ts recentbefore changing anything. See SELinux booleans and how to use setroubleshoot to fix SELinux denials.
8. Test the FTP server
On the server, list the user’s folder with curl (-k skips the certificate check, because the name 127.0.0.1 is not on the certificate):
curl -v --ssl-reqd -k ftp://ftpuser:PASSWORD@127.0.0.1/
The important lines from our AlmaLinux 10.2 lab server (shortened):
< 220 (vsFTPd 3.0.5) > AUTH SSL < 234 Proceed with negotiation. * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / RSASSA-PSS < 230 Login successful. < 200 PROT now Private. < 229 Entering Extended Passive Mode (|||40060|) < 226 Directory send OK.
The passive port (40060) is inside the range from step 4. Uploads into files worked (226), an upload into the top folder was refused with 553 Could not create file., as intended, and folders outside /home/ftpuser/ftp could not be reached.
From your computer, use an FTP client such as FileZilla: host ftp.example.com, port 21, protocol FTP, encryption Require explicit FTP over TLS, and the user’s name and password. See configuring your website in FileZilla.
Common vsftpd errors on AlmaLinux, Rocky Linux and RHEL
530 Non-anonymous sessions must use encryption.: the client tried plain FTP. Choose explicit FTP over TLS.530 Permission denied.(straight after the user name): the user is not in/etc/vsftpd/allowed_users.530 Login incorrect.: wrong password, a shell that is not in/etc/shells(step 2), or an account listed in/etc/vsftpd/ftpusers. See also 530 Login Authentication Failed.500 OOPS: vsftpd: refusing to run with writable root inside chroot(): the user owns or can write to the top folder. Runchown root:root /home/ftpuser/ftpandchmod 755 /home/ftpuser/ftp.500 OOPS: cannot change directory:/home/USERNAME/ftp: the user’sftpfolder does not exist. Create it as in step 2.- With TLS required, clients often show only a TLS error for the two
500 OOPSmessages (curl reportedbad record type), because vsftpd sends them unencrypted. - Login works but the folder listing never arrives: the passive ports are blocked in a firewall, on the server or in front of it.
Logs: with these settings vsftpd writes uploads and downloads to /var/log/xferlog (readable by root only). Add dual_log_enable=YES to also get /var/log/vsftpd.log, which records logins: in our test it showed [ftpuser] FAIL LOGIN: Client "127.0.0.1" for a wrong password and OK LOGIN for a good one. More options in verbose logging for vsftpd.
Related guides: key vsftpd settings explained and firewalld command line reference.
Official documentation: vsftpd.conf options and Certbot user guide.
Ucartz services for this topic
- KVM VPS hosting: NVMe SSD and full root access, with Free Basic Managed Support.
- Unmanaged dedicated servers: full root access and free IPMI KVM, with Free Basic Managed Support.
- Linux server management: hardening, patching, monitoring and automation for Ubuntu, Debian, RHEL and AlmaLinux.
