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.com to the server. Certbot comes from the EPEL repository: on AlmaLinux run dnf install epel-release, then dnf install certbot (certbot 4.2.0 on AlmaLinux 10.2 in our test); on RHEL, enable EPEL first as its documentation describes. Run certbot 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 with firewall-cmd --permanent --add-service=http), the files are saved as /etc/letsencrypt/live/ftp.example.com/fullchain.pem and privkey.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=NO with listen_ipv6=YES is the package default and serves IPv4 and IPv6.
  • chroot_local_user, user_sub_token and local_root lock every user into /home/USERNAME/ftp.
  • userlist_deny=NO turns the user list into an allow list: only users in /etc/vsftpd/allowed_users may log in. The package’s own /etc/vsftpd/user_list is a deny list of system accounts and is no longer used. More in allow or deny users on vsftpd.
  • pasv_min_port and pasv_max_port fix the passive data ports so you can open them in the firewall. Behind NAT, also set pasv_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_2 and 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/files is user_home_t (checked with matchpathcon). If you moved files in from elsewhere, they may keep their old label: reset it with restorecon -Rv /home/ftpuser and check with ls -Z.
  • Only if FTP users must write outside /home, for example into a website folder in /var/www, turn on setsebool -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 with ausearch -m avc -ts recent before 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. Run chown root:root /home/ftpuser/ftp and chmod 755 /home/ftpuser/ftp.
  • 500 OOPS: cannot change directory:/home/USERNAME/ftp: the user’s ftp folder does not exist. Create it as in step 2.
  • With TLS required, clients often show only a TLS error for the two 500 OOPS messages (curl reported bad 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

Was this answer helpful? 0 Users Found This Useful (0 Votes)