Apr 062012

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

The alternative would be OpenSSH 5, where you can give access to sftp only, in a chrooted environment, without using any unofficial OpenSSH patches.

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

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 `/\t’.

Disable anonymous access

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:

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.


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”.


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

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.

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.

Problem: 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 ( 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 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.

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:

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

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

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:

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

Use it like this:

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:

Create the database:

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:

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:

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:

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.

  2 Responses to “Secure FTP with VSFTPD”

  1. THANK YOU!! I have been working for 2 days trying to sort this all out. Your ssl_cyphers_high fixed one of the last problems I was having (RHEL 5.8) getting this to work. Very clear description of PASV/ACTIVE issue. The vsftpd server I was running does not seem t want to work in anything but active mode at the moment, but I expect its firewall is blocking the other connections, at lest using the secure versions.

    I assume the build you are describing (and the one in this RHEL YUM directory) does not actually support validation on the certificates: None of those commands (ssl commands found at some other sights, like validate_cert) are accepted by this version.

    Again, many thanks for posting this.

    • Glad to help. No, the Centos 5 version, the one I use for now, doesn’t have validate_cert, I checked, mine is 2.0.5, validate_cert was added in 2.0.6. It’s probably out there on some repo if you need it though. Personally I wouldn’t bother with it anyway, one reason being that obtaining a signed certificate is too easy for an attacker but might be a bother for the client, the other is that I’m just trying to establish encryption, not authentication.

      Edit: unless, you set up your own certificate authority and accept only certificates signed by you. That would work I guess.

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">