Secure FTP Server with Pure-FTPD, TLS and Virtual Users in MySQL

FTP is outdated and insecure. That said, many people still use it, and many people, most people, set it up insecurely. This guide will take you through setting up a secure ftp server which uses TLS to encrypt usernames and passwords (data transfer too if you want this) and uses virtual users, each of which will get chrooted (jailed) into a directory of your choosing. As Redhat and therefore CentOS do not include Pure-FTPd in the default repositories we'll need to compile Pure-FTPd from source. EPEL does however include Pure-FTPd as well as pam_mysql which is required for vsftpd (Redhat's default FTP server) to authenticate against a MySQL back-end. Why Pure-FTPd over vsftpd? Well, as far as I can tell, vsftpd does not chroot each virtual user into their own directory, it's been a very long time since I've done virtual users with vsftpd so I could be wrong here. The folk behind Pure-FTPd however had virtual users in mind right from the beginning.

Compiling Pure-FTPd

So, download the source code from http://www.pureftpd.org and extract it.

Then run configure with options --with-tls --with-mysql and --with-boring

# ./configure --with-tls --with-mysql --with-boring

When configure is complete, do make install-strip to compile and install.

# make install-strip

VoilĂ , Pure-FTPd is now installed. But we're not done yet. As it is at the moment, it's about as secure as Redhat's default vsftpd install. In other words, any system account can now authenticate and login, sending everything, password included, in clear text across public internet, and gain access to the entire filesystem. PureFTPd doesn't use a config file like most other FTP daemons (although if you install from distro packages you probably will), it uses command line switches to set various things. You will however need a config file to tell it how to connect to MySQL to obtain the user information.

System Account and Permissions

To control who has read and who has write access, we'll add 2 real users and a group to the system. Both will be part of the same group which will have read access and one will have write access to the filesystem.

# groupadd -g 6000 vftp
# useradd -u 6000 -d /var/ftp -s /sbin/nologin -g vftp vftp
# useradd -u 6001 -d /var/ftp -s /sbin/nologin -g vftp vftpro
# mkdir /var/ftp
# chown vftp:vftp /var/ftp
# chmod 3750 /var/ftp

MySQL

Next, create the MySQL database for storing virtual users. These are all MySQL commands.

CREATE DATABASE ftp;

USE ftp;

CREATE TABLE users (
  User VARCHAR(64) BINARY NOT NULL,
  Password VARCHAR(256) BINARY NOT NULL,
  Uid INT(11) NOT NULL default '6000',
  Gid INT(11) NOT NULL default '6000',
  Dir VARCHAR(256) BINARY NOT NULL default '/var/ftp',
  PRIMARY KEY  (User)
);

GRANT SELECT, INSERT, UPDATE ON ftp.* TO 'pureftpd_u'@'localhost' IDENTIFIED BY 'P@$$w0rD!' ;
GRANT SELECT, INSERT, UPDATE ON ftp.* TO 'pureftpd_u'@'127.0.0.1' IDENTIFIED BY 'P@$$w0rD!' ;

FLUSH PRIVILEGES;

Add users for testing. Note: Password field needs to be encrypted with crypt or encrypt.

INSERT INTO `ftp`.`users` (
`User` ,`Password` ,`Uid` ,`Gid` ,`Dir`
) VALUES (
'upload', ENCRYPT( 'upload' ) , '6000', '6000', '/var/ftp'
);

INSERT INTO `ftp`.`users` (
`User` ,`Password` ,`Uid` ,`Gid` ,`Dir`
) VALUES (
'download', ENCRYPT( 'download' ) , '6001', '6000', '/var/ftp'
);

Next, we need a config file so that pureftpd knows how to connect to MySQL.

Copy this into /etc/pure-ftpd-mysql.conf and edit to suite your needs.

# Sample Pure-FTPd Mysql configuration file. #
# See README.MySQL for explanations.         #

# Optional: MySQL server name or IP. Don't define this for unix sockets.
MYSQLServer     localhost

# Optional: MySQL port. Don't define this if a local unix socket is used.
MYSQLPort       3306

# Optional: define the location of mysql.sock if the server runs on this host.
MYSQLSocket     /var/lib/mysql/mysql.sock

# Mandatory: user to bind the server as.
MYSQLUser       pureftpd_u

# Mandatory: user password. You must have a password.
MYSQLPassword   P@$$w0rD!

# Mandatory: database to open.
MYSQLDatabase   ftp

# Mandatory: how passwords are stored
# Valid values are: "cleartext", "crypt", "md5" and "password"
# ("password" = MySQL password() function)
# You can also use "any" to try "crypt", "md5" *and* "password"
MYSQLCrypt      crypt

# In the following directives, parts of the strings are replaced at
# run-time before performing queries:
#
# \L is replaced by the login of the user trying to authenticate.
# \I is replaced by the IP address the user connected to.
# \P is replaced by the port number the user connected to.
# \R is replaced by the IP address the user connected from.
# \D is replaced by the remote IP address, as a long decimal number.
#
# Very complex queries can be performed using these substitution strings,
# especially for virtual hosting.

# Query to execute in order to fetch the password
MYSQLGetPW      SELECT Password FROM users WHERE User="\L"

# Query to execute in order to fetch the system user name or uid
MYSQLGetUID     SELECT Uid FROM users WHERE User="\L"

# Optional: default UID - if set this overrides MYSQLGetUID
# MYSQLDefaultUID 6000

# Query to execute in order to fetch the system user group or gid
MYSQLGetGID     SELECT Gid FROM users WHERE User="\L"

# Optional: default GID - if set this overrides MYSQLGetGID
# MYSQLDefaultGID 6000

# Query to execute in order to fetch the home directory
MYSQLGetDir     SELECT Dir FROM users WHERE User="\L"

# Optional: query to get the maximal number of files
# Pure-FTPd must have been compiled with virtual quotas support.
# MySQLGetQTAFS  SELECT QuotaFiles FROM users WHERE User="\L"

# Optional: query to get the maximal disk usage (virtual quotas)
# The number should be in Megabytes.
# Pure-FTPd must have been compiled with virtual quotas support.
# MySQLGetQTASZ  SELECT QuotaSize FROM users WHERE User="\L"

# Optional: ratios. The server has to be compiled with ratio support.
# MySQLGetRatioUL SELECT ULRatio FROM users WHERE User="\L"
# MySQLGetRatioDL SELECT DLRatio FROM users WHERE User="\L"

# Optional: bandwidth throttling.
# The server has to be compiled with throttling support.
# Values are in KB/s .
# MySQLGetBandwidthUL SELECT ULBandwidth FROM users WHERE User="\L"
# MySQLGetBandwidthDL SELECT DLBandwidth FROM users WHERE User="\L"

# Enable ~ expansion. NEVER ENABLE THIS BLINDLY UNLESS:
# 1) You know what you are doing.
# 2) Real and virtual users match.
# MySQLForceTildeExpansion 1

# If you upgraded your tables to transactionnal tables (Gemini,
# BerkeleyDB, Innobase...), you can enable SQL transactions to
# avoid races. Leave this commented if you are using the
# traditionnal MyIsam databases or old (< 3.23.x) MySQL versions.
# MySQLTransactions On

TLS Encryption

Now, create a certificate for pureftpd to use for TLS. It must be located in /etc/ssl/private/pure-ftpd.pem If you would like to change where it is stored, this needs to be done at compile time. See references below.

# mkdir -p /etc/ssl/private
# openssl req -x509 -nodes -newkey rsa:1024 -keyout \
  /etc/ssl/private/pure-ftpd.pem \
  -out /etc/ssl/private/pure-ftpd.pem
# chmod 600 /etc/ssl/private/*.pem

Startup and testing

Now that we have everything required for our FTP server, all that's left is to fire it up with the command:

# pure-ftpd -A -c 50 -E -H -i -l mysql:/etc/pure-ftpd-mysql.conf -R -u 6000 -Y 2 -Z

Test it, and once working add -B to daemonize the service. man pure-ftpd for more options. If you want it to start at boot, add it to rc.local (Redhat users) or to it's equivalent for for non Redhat users. Testing, use lftp from localhost or use a client that supports explicit TLS. The user upload should be able to browse files and upload, the user download should be able to only browse and read files, but not upload anything. Remember, this is controlled by which UID they virtual user get's assigned.

See the official Pure-FTPd documentation at http://www.pureftpd.org/project/pure-ftpd/doc

Micro Banner Micro Banner Micro Banner Micro Banner Micro Banner Micro Banner