Build a Secure FTP Server with CentOS and vsftpd

In this article, I am going to outline the steps that I have taken to create a secure and stable FTP server for general purpose file transfer etc. I am not using FTPS (FTP Secure) in this article though it is possible using vsftpd. I have used a number of techniques in this article to secure the server that can be found here as well as some tweaks to SELinux that I will explain.


I take no responsibility for any damage that may result from following this guide. Ensuring that you take the appropriate measures to secure your server/infrastructure is paramount. I also recommend thoroughly testing this configuration before production use.

1. A basic (Preferably intermediate) understanding and ability to administer Linux.
2. A basic understanding of Networking as well as the FTP protocol and Firewalls.

For this article, I will be using CentOS 6.5 Minimal. I chose this OS because it has very few services enabled by default and is inherently secure, well documented, has good community support and is a RedHat Derivative. I am not going to go into detail on the installation, i assume you are able to install the OS, set the host name and configure the IP address(es) as needed. As a security precaution, also update the OS completely before and after the installation and configuration of the software.

#> yum update -y
#> reboot

As mentioned, it is important to secure the operating system as much as possible (here is a great article). Below I will outline the changes that i made to the system.

I am using IPTables firewall to lock down the network. Though I have a hardware firewall in front of the server as well, this is recommended to further secure things – and remember, before making any changes, BACKUP the config files you are about to edit!

#> cp /etc/sysconfig/iptables /etc/sysconfig/iptables.orig
#> vi /etc/sysconfig/iptables
Below are my rules:
:RH-Firewall-1-INPUT - [0:0]
-A INPUT -j RH-Firewall-1-INPUT
-A FORWARD -j RH-Firewall-1-INPUT
-A RH-Firewall-1-INPUT -i lo -j ACCEPT
-A RH-Firewall-1-INPUT -p icmp --icmp-type echo-reply -j ACCEPT
-A RH-Firewall-1-INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
-A RH-Firewall-1-INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
# Accept Pings
-A RH-Firewall-1-INPUT -p icmp --icmp-type echo-request -j ACCEPT
# Log anything on eth0 claiming it's from a local or non-routable network
# If you're using one of these local networks, remove it from the list below
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP SPOOF A: "
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP SPOOF B: "
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP SPOOF C: "
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP MULTICAST D: "
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP SPOOF E: "
-A INPUT -i eth0 -d -j LOG --log-prefix "IP DROP LOOPBACK: "
# Accept any established connections
-A RH-Firewall-1-INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Accept ssh traffic. Restrict this to known ips if possible.
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 21 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 20 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 50000:51000 -j ACCEPT
#Log and drop everything else
-A RH-Firewall-1-INPUT -j LOG
-A RH-Firewall-1-INPUT -j REJECT --reject-with icmp-host-prohibited

#> service iptables restart

We now have a nice shiny set of IPTables rules that will work to allow us SSH and FTP access to the box.

The next item that we will look at are the sysctl rules that apply to tcp. Here we will add some rules that dictate how tcp should operate as well as what type of traffic and routing is allowed.

#> cp /etc/sysctl.conf /etc/sysctl.orig
#> vi /etc/sysctl.conf

Below is what my sysctl.conf file looks like. There are a large number of options available that can be applied using this configuration file that are outside the scope of this document. For further reading, see the documentation (the man page is useless!).

# Kernel sysctl configuration file for Red Hat Linux
# For binary values, 0 is disabled, 1 is enabled. See sysctl(8) and
# sysctl.conf(5) for more details.

# Controls IP packet forwarding
net.ipv4.ip_forward = 0

# Controls source route verification
net.ipv4.conf.default.rp_filter = 1

# Do not accept source routing
net.ipv4.conf.default.accept_source_route = 0

# Controls the System Request debugging functionality of the kernel
kernel.sysrq = 0

# Controls whether core dumps will append the PID to the core filename.
# Useful for debugging multi-threaded applications.
kernel.core_uses_pid = 1

# Controls the use of TCP syncookies
net.ipv4.tcp_syncookies = 1

# Disable netfilter on bridges.
net.bridge.bridge-nf-call-ip6tables = 0
net.bridge.bridge-nf-call-iptables = 0
net.bridge.bridge-nf-call-arptables = 0

# Controls the default maxmimum size of a mesage queue
kernel.msgmnb = 65536

# Controls the maximum size of a message, in bytes
kernel.msgmax = 65536

# Controls the maximum shared segment size, in bytes
kernel.shmmax = 68719476736

# Controls the maximum number of shared memory segments, in pages
kernel.shmall = 4294967296

# Custom Lockdown Configurations.
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.tcp_max_syn_backlog = 1280
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_timestamps = 0

As with many of my other articles, and coming from a Debian background, I prefer to use sudo as opposed to having a root user enabled on my systems. Note that this ban byte you as if you disable the root user (as outlined below) and you need to enter single user mode – it is time for a live cd! This is not really a problem in a virtual environment but may be in a physical environment with a headless server. Use your discretion when following these steps.

#> useradd USERNAME - I assume you know how to add a user here.
#> groupadd sudo
#> usermod -G sudo USERNAME
#> visudo

Add the following line to the sudo file:

%sudo  ALL (ALL:ALL) ALL

This will allow members of the sudo group to execute any command as root.  Now, we will disable the root account below. Skip this step if you are uneasy about it. You may also want to test the sudo capability of the user you just created before doing this.

#> passwd -l root

At this point, it is necessary to reboot the system and test everything to make sure that there are no issues at reboot.

Now that we have completed a basic lock down of the server, we can move on to installing and configuring the FTP server itself. Here we are using vsftpd for it’s security and ease of use.

#> yum install vsftpd -y
#> chkconfig vsftpd on

Thats it! vsftpd is now installed. Next we will configure it. This section includes some items that are custom to my system and may not apply to you but the principals should still apply to your configurations. Modify the commands to match your paths etc.

#> cp /etc/vsftpd/vsftpd.conf /etc/vsftpd/vsftpd.conf
#> vi /etc/vsftpd/vsftpd.conf

Uncomment/Change/Add the following values accordingly.


#> service vsftpd restart

By default, root is not allowed to log into the server using FTP (Which is a good thing right!) so we need to create a user. Here is where your configuration may differ from mine. I have an iSCSI LUN mounted on /var/ftp so that is where I am placing my user home directories when creating users. This poses multiple problems, for which I have solutions outlined below.

#> useradd -d /var/ftp/ftpuser ftpuser
#> chown -R ftpuser.ftpuser /var/ftp/ftpuser

You may be thinking, “Looks Easy, what is this problem you speak of”. Well my friends, it is a little thing called SELinux. If you were to use the ftp command to try to log into the server using this newly created user, you will most likely receive an error similar to: “500 OOPS: cannot change directory”. This is because selinux is isolating the ftp process from accessing user home directories.

(You could disable selinux all together – but I do not recommend it)

#> setsebool -P ftp_home_dir on
#> setsebool -P allow_ftp_full_access on

#> chcon -Rv –type user_home_dir_t /var/ftp/ftpuser

Whew! Seems a bit much to create a user but we are doing things with security in mind, and with increased security comes decreased usability (or so I have been told). Now that your user has been created, you can now test to make sure you are able to upload files. If you are not, check the logs (/var/log/audit/audit.log for selinux) as well as all of the configurations outlined above.

You now have a shiny new secure Linux FTP server for transferring files or even remote backups. I hope you enjoyed this article. Feel free to post questions.

Leave a Reply

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload the CAPTCHA.