Secure FTP with VSFTPD

Setting up an SSL-encrypted FTP server using Very Secure FTP Daemon. Configuration snippets, explanations and potential problems.

The alternative would be OpenSSH, where you can give access to sftp only, in a chrooted environment.

If you get the following error when attempting to start vsftpd:

500 OOPS: missing value in config file for:

you probably have an empty line somewhere that starts with a Tab. In vim you can see control characters if you :set list, hide them with :set nolist, and you can search for tabs with /\.

Disable anonymous access

### ANONYMOUS
# No anonymous users allowed at all
# there are a bunch of other anon_ and chown_ options
# if this ever gets enabled
#
# Allow anonymous FTP?
# Default: YES
anonymous_enable=NO
# Only applies if ssl_enable is active. If set to YES, anonymous
# users will be allowed to use secured SSL connections.
# Default: NO
allow_anon_ssl=NO
#
# You may specify a file of disallowed anonymous e-mail
# addresses. Apparently useful for combatting certain DoS attacks.
#deny_email_enable=YES
# (default follows)
#banned_email_file=/etc/vsftpd/banned_emails
#

Set up local users access

Local users are users that are defined on the server, i.e. they exist in /etc/passwd and related files. They don’t need to have shell access, but they do need to exist as VSFTPD will authenticate them against the server.

Setting chroot_local_users to YES would place all local users in a jail in their home directories. This can have security implications if your users also have shell access. Instead, the combination of userlist_ and chroot_list_ options below, together with a nologin shell will only give ftp access to specific users and jail them. Users with shell access can transfer files over ssh. The userlist_file and chroot_list_file can be the same, that way any user with ftp access will also be jailed.

A sequence of commands like:

useradd -s /sbin/nologin -d /var/www/html/mysite -m webmaster
passwd webmaster
echo "webmaster" >> /etc/vsftpd/allowed.userlist
echo "webmaster" >> /etc/vsftpd/chroot_list

would add the user “webmaster” to the system and give it chrooted ftp access to the directory where files for “mysite” are stored.

SECURITY WARNING: Even though the user can’t login, it will still be validated. So, for example, SSH tunneling might be possible, or Samba, or LDAP, or a bunch of other services running on the same system might consider that user as being authorized. These passwords get sent by email in clear text many times and the (legitimate) user can’t even change the password unless there’s a special mechanism for that in place, so by using a local user you are probably exposing yourself to a whole lot of potential trouble. Consider a combination of firewall/vpn for those other services or, better yet, set up virtual users instead.

### LOCAL USERS
# Uncomment this to allow local users to log in.
# If enabled, normal user accounts in /etc/passwd 
# (or wherever your PAM config references) may be used to log in. 
# This must be enable for any non-anonymous login to work,
# including virtual users
# Default: NO
local_enable=YES
#
# Default umask for local users is 077. You may wish to change this to
# 022 if your users expect that (022 is used by most other ftpd's)
local_umask=022
#
# If enabled, vsftpd will use userlist_file to determine if the user
# will be allowed or denied access before asking for a password. 
# See also userlist_deny.
# Default: NO 
userlist_enable=YES
#
# If YES (default), users in the list will be denied access
# If NO, users will be denied UNLESS they are in the list
# change userlist_file according to this value
userlist_deny=NO
#
# This option is the name of the file loaded when the
# userlist_enable option is active.
userlist_file=/etc/vsftpd/allowed.userlist
#userlist_file=/etc/vsftpd/denied.userlist
#
### CHROOT
# If set to YES, local users will be (by default) placed in a
# chroot() jail in their home directory after login.
# Defaul: no
#chroot_local_user=YES
#
# You may specify an explicit list of local users to chroot()
# to ther home directory. If chroot_local_user is YES, then
# this list becomes a list of users to NOT chroot().
chroot_list_enable=YES
# (default follows)
chroot_list_file=/etc/vsftpd/chroot_list

Logging

### LOGGING
#
# The target log file can be vsftpd_log_file or xferlog_file.
# This depends on setting xferlog_std_format parameter
xferlog_enable=YES
#
# The name of log file when xferlog_enable=YES and xferlog_std_format=YES
# WARNING - changing this filename affects /etc/logrotate.d/vsftpd.log
#xferlog_file=/var/log/xferlog
#
# Switches between logging into vsftpd_log_file and xferlog_file files.
# NO writes to vsftpd_log_file, YES to xferlog_file
xferlog_std_format=YES
#

Other settings

There is something to be said about ftpd_banner and banner_file. A banner is text that will be displayed to users that access the server before anything else, before they log in. By default the banner is something like “vsFTPD 2.0.5”. This will reveal the software used and version to anyone, which isn’t necessarily a good idea, but that’s not all. A banner can have legal implications, so changing it to something informing the users that unauthorized access is prohibited (if that is the case) and all activity is logged and monitored might be better than just “OHAI THERE”.

### MISC
# This controls whether any FTP commands which change the filesystem
# are allowed or not. These commands are: STOR, DELE, RNFR, RNTO,
# MKD, RMD, APPE and SITE.
# Default: NO
write_enable=YES
#
# Activate directory messages - messages given to remote users when
# they go into a certain directory. Default file is .message
# Default: NO
#dirmessage_enable=YES
#
# Make sure PORT transfer connections originate from port 20 (ftp-data).
# Enabled for compatibility
# Default: NO
connect_from_port_20=YES
#
# You may change the default value for timing out an idle session.
#idle_session_timeout=600
#
# You may change the default value for timing out a data connection.
#data_connection_timeout=120
#
# It is recommended that you define on your system a unique user which
#  the ftp server can use as a totally isolated and unprivileged user.
# Default: nobody
nopriv_user=ftp
#
# Enable this and the server will recognise asynchronous ABOR requests.
# Not recommended for security (the code is non-trivial). Not enabling
# it, however, may confuse older FTP clients.
#async_abor_enable=YES
#
# By default the server will pretend to allow ASCII mode but in fact
# ignore the request. Turn on the below options to have the server
# actually do ASCII mangling on files when in ASCII mode.
# Beware that on some FTP servers, ASCII support allows a denial
# of service attack (DoS) via the command "SIZE /big/file" in ASCII
# mode. vsftpd predicted this attack and has always been safe,
# reporting the size of the raw file.
# ASCII mangling is a horrible feature of the protocol.
#ascii_upload_enable=YES
#ascii_download_enable=YES
#
# You may fully customise the login banner string:
#ftpd_banner=Welcome to blah FTP service.
banner_file=/etc/vsftpd/vsftpd.banner
#
# You may activate the "-R" option to the builtin ls. This is disabled
# by default to avoid remote users being able to cause excessive I/O
# on large sites. However, some broken FTP clients such as "ncftp"
# and "mirror" assume the presence of the "-R" option, so there is
# a strong case for enabling it.
ls_recurse_enable=YES
#
# When "listen" directive is enabled, vsftpd runs in standalone mode and 
# listens on IPv4 sockets. This directive cannot be used in conjunction 
# with the listen_ipv6 directive.
listen=YES
#
# This directive enables listening on IPv6 sockets. To listen on
# IPv4 and IPv6 sockets, you must run two copies of vsftpd whith
# two configuration files.
# Make sure that one of the listen options is commented !!
#listen_ipv6=YES

pam_service_name=vsftpd

Encryption

In order to have encrypted connections vsftpd has to be compiled against openssl. To check if it is:

ldd /usr/sbin/vsftpd | grep ssl
libssl.so.6 => /lib/libssl.so.6 (0x001e0000)

If some version of libssl is there, SSL is a go.

Check this post on how to generate a certificate. Note that common name should be the name of your server.

There are two ways of establishing an encrypted connection, implicit and explicit.

In implicit mode, or FTPS, the client is expected to immediately initiate encryption, if that doesn’t happen the server should drop connection. In this mode the servers listens for SSL/TLS connections on port 990, with 989 being the default data channel, allowing for clear text connections on port 21. This mode is considered deprecated.

In explicit mode, or FTPES, after the connection on port 21 is established the client should initiate encrypted mode using the AUTH command then the client and server establish a common known cipher to use. If the client doesn’t send an AUTH, the server has the option to continue unencrypted or tell the client that it needs encryption and drop the connection. Setting force_*_ssl options in vsftpd.conf ensures that clear text communication isn’t allowed in the respective cases.

Default SSL ciphers used by vsftp are “DES-CBC3-SHA”, considered deprecated and not used by newer versions of some FTP clients, like FileZilla, which will fail the connection in this case. Using ssl_ciphers=HIGH fixes the problem.

### SSL
#
# Enable SSL. VSFTPD needs to be compiled against OpenSSL
# Default: NO
ssl_enable=YES
#
# The RSA certificate to use for SSL encrypted connections
rsa_cert_file=/etc/vsftpd/server.crt
#
# The location of the RSA private key to use for SSL encrypted
# connections. If this option is not set, the private key is
# expected to be in the same file as the certificate.
rsa_private_key_file=/etc/vsftpd/server.key
#
# Force encrypted logins for anonymous users
# Default: NO
#force_anon_logins_ssl=YES
#
# Force encrypted data transfers for anonymous
# Default: NO
#force_anon_data_ssl=YES
#
# Force encrypted logins for non-anonymous
# Default: YES
force_local_logins_ssl=YES
#
# Force encrypted data transfers for non-anonymous
# Default: YES
force_local_data_ssl=YES
#
# If enabled, an SSL handshake is the first thing expect on all
# connections (FTPS protocol). To support explicit SSL and/or
# plain text too, a separate vsftpd listener process should be run.
# This option doesn't exist on CentOS 5 vsftpd
# Default: NO 
#implicit_ssl=YES
#
# secure default, YES, but may brake clients
# Not available in 2.0.5
#require_ssl_reuse=NO
#
# By default it will request but not require a client certificate
#ssl_request_cert=YES
#ssl_require_cert=NO
#
# Permit TLS v1 protocol connections. TLS v1 connections are preferred
# Default: YES
ssl_tlsv1=YES
# Permit SSL v2 protocol connections. TLS v1 connections are preferred
# Default: NO
ssl_sslv2=NO
# permit SSL v3 protocol connections. TLS v1 connections are preferred
# Default: NO
ssl_sslv3=NO
#
# Newer versions of some clients, like FileZilla, don't accept the
# default ciphers see `man ciphers' for details
# Default: DES-CBC3-SHA
ssl_ciphers=HIGH

Passive mode and the firewall

FTP is different than most other protocols because it uses two connections, one for commands and a second one for data transfers. The way it works is, when a client connects to a server it first establishes a “control connection”, on port 21. That connection remains active throughout the session and is used for sending commands, like LS or RETR to the server. When the client asks for a data transfer, for example RETR , a second connection is opened and used for the transfer. There are two ways to open the data connection, active mode or passive mode.

In active mode the client will listen for connections on a local port which is communicated to the server using the PORT command. When the client gives a command for a data transfer, the server will initiate a connection to the address provided by the client. It’s called “active” because the server actively initiates the transfer.

In passive mode the client sends the PASV command to the server and the server will respond with an address for the client to connect to. The client then gives a command to transfer a file or get a listing, and establishes a secondary connection to the address returned by the server. It’s called “passive” because in this mode the server passively waits for the data connection to be established.

In order for any of those two to work the firewalls/routers between the two computers must let at least one of those connections through. Because we have control over the server but we don’t know what the network configuration on the client end is and because most clients default to passive mode, we should make sure that it works. Passive depends on the server firewall, active depends on the client.

  • Note: if unencrypted connections work, but encrypted ones time out or fail:

    Most stateful firewalls will be aware of the quirks of FTP protocol and will let the data connection through by inspecting the packets/commands sent by the control connection even if those ports aren’t explicitly open, but if the control connection is encrypted the firewall won’t be able to read the packets and won’t know that it should let the newly initiated connection get by, so we need to specify a range of ports for it that will be open in the firewall. This is done using pasv_min_port and pasv_max_port.

    Also, the same applies to NATed FTP servers. For example, in passive mode the server responds to the PASV command with something like “227 Entering Passive Mode (192,168,10,10,195,86)”. The first four numbers are the server’s IP address that the client should connect to (192.168.10.10) and the last two are the port (195*256+86 means port 50006). Now, a NAT router/firewall aware of the FTP protocol will change that response so that it lists the public IP of the server (for example 133.156.217.20) and will port forward the incoming connection from the client to the server behind it. But if the connection is encrypted, the router can’t do that, so we need to specify the external IP of the server using pasv_address.

### PORTS / PASSV
#
# Set to NO if you want to disallow the PASV method of obtaining a data
# connection
# Default: YES
#pasv_enable=NO
#
# Maximum and minimum port to allocate for PASV data connections
# these ports will be open in firewall and forwarded on router
# Default: 0
pasv_min_port=50000
pasv_max_port=50009
#
# This optin can be used to provide the IP address that will be advertised in
# PASV commands, this should be the public IP of the server
# Default: (none - the address is taken from the incoming connected socket)
pasv_address=193.231.80.194

Virtual Users

Virtual users may not exist in /etc/passwd and as such they will not be authenticated by the system, which makes them more secure, as a cracked user might only have access to the FTP server. In order to use this feature vsftpd needs to be compiled with PAM support. Check by running:

ldd /usr/sbin/vsftpd | grep pam
libpam.so.0 => /lib/libpam.so.0 (0x00416000)

If the result is something like the above, you’re good to go.

The way it works is, after receiving the user/password pair from the client vsftpd will ask PAM if they are valid. PAM will use the authentification mechanism the sysadmin tells it to in order to check and returns an answer. What that means is that one could use anything, from plain text files to MySQL/RADIUS/LDAP, or a combination of those, in order to validate FTP users, provided the right PAM module is enabled.

There is pam_pwdfile, which uses a simple file with encrypted password in the same format apache uses, but since it’s not available in CentOS 5 by default, I settled for using a Berkeley DB. Note that at the time of this writing, reportedly, pam_pwdfile wasn’t working on CentOS 6, but there is a patch for it, check the repo.

In order to work with Berkley DB we will need db_load, in CentOS 5 it’s in db4_utils package, so

yum install db4_utils

There’s also the PAM module, it’s called pam_userdb and it’s probably already installed

locate pam_userdb.so
/lib/security/pam_userdb.so

Initially the module didn’t know about encrypted passwords, so passwords were stored in clear text. It was patched in 2004 and now it is capable of reading encrypted passwords using crypt(3). The provided encryption is quite weak, but still better than nothing, from what I could gather by reading the pam_userdb sources that I found, the additional encryption algorithms available to glibc2 crypt() cannot be used by this module. Unfortunate.

Anyway, we’re gonna create a database containing “user:encrypted_password” pairs, a new PAM file which is gonna tell it where the database is and that it’s encrypted and configure VSFTPD for virtual users.

First, we’re gonna need the password in encrypted form. Note that only the first 8 characters of the password matter, so use 8char passwords. If openssl is available:

openssl passwd -crypt userPass
M8VCOMF2EzK82

Alternatively, here’s a small python script that will return the proper string:

import crypt

passwd = raw_input("Password: ")
salt = raw_input("Salt: ")

print(crypt.crypt(passwd, salt))

Use it like this:

python3 genpass.py
Password: userPass
Salt: M8
M8VCOMF2EzK82

Notice that it asks for a salt. That’s a two character string, openssl generates a random salt, for python you will need to provide it.

Now that we have the password hash, let’s create the database. We’ll need a text file, say ‘vsftpd.users’, each two lines representing a user:password pair, like this:

ftpuser1
M8VCOMF2EzK82
ftpuser2
FwxXEXnl1cZTM

Create the database:

db_load -T -t hash -f vsftpd.users vsftpd.users.db

db_load APPENDS to the database, does not delete or replace users, so you will need to make sure ‘vsftpd.users.db’ doesn’t exist. Also, make sure the files are chmoded to 600. You can delete the text file or keep it around in case you need to recreate the database later, but better find out how to delete/modify records.

Create a new file, /etc/pam.d/vsftpd_virtual_users, in which we tell PAM to check the database for valid users:

#%PAM-1.0
auth       required     pam_userdb.so db=/etc/vsftpd/vsftpd.users crypt=crypt
account    required     pam_userdb.so db=/etc/vsftpd/vsftpd.users crypt=crypt
session    required     pam_loginuid.so

Note the “crypt=crypt” arguments, this is what tells pam_userdb that the password is stored in encrypted form in the database. Also, the ‘db’ parameter omits the extension, so the database must have the .db extension.

Finally, vsftpd.conf:

### VIRTUAL USERS
# the name of the PAM service vsftpd will use
pam_service_name=/etc/pam.d/vsftpd_virtual_users
#
# enable virtual users
guest_enable=YES
#
# enable local users
local_enable=YES
#
# virtual users use local users privileges
virtual_use_local_privs=YES
#
# enable write commands
write_enable=YES
#
# the local user that virtual users will impersonate
# this user needs to have proper access to virtual user's home dir
# Default: ftp
guest_username=ftp
#
# Automatically generate a home directory for each virtual user,
# based on a template For example, if the home directory of the
# real user specified via guest_username is /home/virtual/$USER,
# and user_sub_token is set to $USER, then when virtual user fred
# logs in, he will end up (usually chroot()’ed) in the directory
# /home/virtual/fred. This option also takes affect if local_root
# contains user_sub_token
user_sub_token=$USER
#
# User home directory. Needs to exist
local_root=/home/vftp/$USER
#
# Chroot user and lock down to their home dirs
chroot_local_user=YES
#
# Hide ids from user
hide_ids=YES

In this example the home directory is /home/vftp/, it needs to be set +rw by ftp, the user that will be impersonated. We can create symlinks under this directory for users that need to access other directories on the system, like /var/www/html/website. These other directories also need to have proper permissions. Alternatively, per-user config files can be used:

Per-user config files

From the man page:

user_config_dir
    This powerful option allows the override of any config option
    specified in the manual page, on a per-user basis. Usage is
    simple, and is best illustrated with an example. If you set
    user_config_dir to  be /etc/vsftpd_user_conf and then log on as
    the user "chris", then vsftpd will apply the settings in the file
    /etc/vsftpd_user_conf/chris for the duration of the session.
    The format of this file is as detailed in vsftpd.conf manual page

    PLEASE NOTE that not all settings are effective on a per-user basis.
    For example, many settings only prior to the user’s session being
    started. Examples of settings which will not affect any behaviour
    on a per-user basis include listen_address, banner_file, max_per_ip,
    max_clients, xferlog_file, etc.
    Default: (none)

Note: Apparently VSFTPD can’t do both virtual and local users, when guest_enable=YES all non-anonymous logins get mapped to guest_username. Suppose it’s possible to allow both to login through PAM, or simply create virtual users for all local users, but still not the ideal setup.