vsftpd on OpenBSD HOWTO

Stefan Pettersson

Permission to use, copy, modify, and/or distribute this documentation for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE DOCUMENTATION IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS DOCUMENTATION INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS DOCUMENTATION.

Revision History
Revision 1.08 Jan 2011stef
First public release.

Abstract

This HOWTO explains how vsftpd can be installed and configured on OpenBSD in a simple and secure way. Since both vsftpd and OpenBSD share security as a primary goal, mistakes in the installation and configuration are likely to pose larger security risks than mistakes in the source code. The configuration presented will serve local users their, chrooted, home directories, over TLS, and anonymous users a shared /pub directory (optionally with an upload/ directory). All while trying to fit nicely in the OpenBSD operating system.


Table of Contents

Installation
Getting source packages
Verifying signatures
Building
Preparing for installation
Installing
Testing the build
Configuration
Basic configuration
Local user access
Anonymous user access
Post-configuration
Files in /etc
Generating certificates
vsftpd.conf
Testing

Installation

The OpenBSD package repository currently holds vsftpd 2.0.5 for OpenBSD 4.5 and vsftpd 2.2.2 for OpenBSD 4.8 while the most current version is vsftpd 2.3.2.

Here we will install from source since it's more illustrative. If you choose to use a package instead, be sure to give pkg_add the -vvv option so that you get a picture of how vsftpd is being set up.

Getting source packages

We download the vsftpd tarball and its corresponding signature using ftp from the official ftp server vsftpd.beasts.org:

$ ftp -o vsftpd-2.3.2.tar.gz ftp://vsftpd.beasts.org/users/cevans/vsftpd-2.3.2.tar.gz
[...]
$ ftp -o vsftpd-2.3.2.tar.gz.asc ftp://vsftpd.beasts.org/users/cevans/vsftpd-2.3.2.tar.gz.asc
[...]

Verifying signatures

Now we want to verify the detached cryptographic signature held in the .asc file. First, find out what key ID signed the package by trying to verify it. Then download the corresponding public key from a key server and use it to verify the signature. The gpg command is provided by the package gnupg.

$ gpg --verify vsftpd-2.3.2.tar.gz.asc
gpg: Signature made Fri Aug 20 01:37:24 2010 CEST using DSA key ID 3C0E751C
gpg: Can't check signature: public key not found
$ gpg --recv-keys 3c0e751c
gpg: requesting key 3C0E751C from hkp server keys.gnupg.net
gpg: /home/stef/.gnupg/trustdb.gpg: trustdb created
gpg: key 3C0E751C: public key "Chris Evans <chris@scary.beasts.org>" imported
gpg: Total number processed: 1
gpg:               imported: 1
$ gpg --verify vsftpd-2.3.2.tar.gz.asc
gpg: Signature made Fri Aug 20 01:37:24 2010 CEST using DSA key ID 3C0E751C
gpg: Good signature from "Chris Evans <chris@scary.beasts.org>"
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 8660 FD32 91B1 84CD BC2F  6418 AA62 EC46 3C0E 751C

The verification checked out. The warning message was expected, it means that GPG verified that the public key we downloaded from the key server was indeed used to sign the package we downloaded from beasts.org but that it can't promise that the key really belongs to Chris Evans. There's not much we can do about it really short of meeting Chris at some conference, verifying his identity and getting the key from him in person.

Building

Before building vsftpd we will want to edit builddefs.h to match our requirements. Make sure SSL is defined and that PAM as well as TCPWRAPPERS is undefined. OpenBSD does not use PAM and we won't use TCPWRAPPERS in this HOWTO, so we might as well skip them. builddefs.h should look something like this when you're finished:

#ifndef VSF_BUILDDEFS_H
#define VSF_BUILDDEFS_H

#undef VSF_BUILD_TCPWRAPPERS
#undef VSF_BUILD_PAM
#define VSF_BUILD_SSL

#endif /* VSF_BUILDDEFS_H */

To build we just run make which should output a vsftpd binary.

$ make
[...]
$ ls -l vsftpd
-rwxr-xr-x  1 stef  staff  101444 Jan  6 11:50 vsftpd
$ file vsftpd
vsftpd: ELF 32-bit LSB executable, Intel 80386, version 1, for OpenBSD, dynamically linked (uses shared libs), stripped

Preparing for installation

Before copying the binaries and taking it for a test drive there are a few things to prepare. First, vsftpd will need a low-privileged user and group to run as. Also, we need an ftp user account to use for anonymous users.

We follow OpenBSD's convention of using an underscore as prefix for user and group names used by daemons. By default, groupadd will give the group an ID starting from 1000 but we'll place it in the range 500-999 to make it stand out from the regular groups used by users. Just make sure it isn't defined in /etc/group or /etc/passwd already since we're going to use the same number as UID for the user.

Also, we create a group that will be able to read the private key file corresponding to the certificate used for FTP over SSL/TLS. More about this later on in the HOWTO.

# groupadd -g 502 _vsftpd
# groupadd ssl

We follow the same conventions for the low-privileged user account, set its primary group to _vsftpd and make it a member of the ssl group. Additionally we set the home directory to /var/empty (to which the user and group have no write permissions), the login class to daemon (check /etc/login.conf for more information),the GECOS field to a descriptive string and the shell to /sbin/nologin.

# useradd -u 502 -g _vsftpd -G ssl -d /var/empty -L daemon -c "vsftpd daemon,,," -s /sbin/nologin _vsftpd

To support anonymous FTP users we need an account called ftp as well. It's more or less the same as _vsftpd but not in the daemon class and its home directory is the anonymous FTP root. In our case, /pub.

# groupadd -g 503 ftp
# useradd -m -u 503 -g ftp -d /pub -c "Anonymous FTP user,,," -s /sbin/nologin ftp

Installing

Now there are only three things to install; (1) the binary, (2) the man pages and (3) the configuration file. This is handled by the Makefile and you can review what it'll do by checking its install target (it won't install the sample configuration file). We'll install it by hand though:

# install -m 555 -o root -g bin vsftpd /usr/local/sbin
# install -m 444 -o root -g bin vsftpd.8 /usr/local/man/man8
# install -m 444 -o root -g bin vsftpd.conf.5 /usr/local/man/man5

Testing the build

Test if the manual pages are located and if the binary runs as expected.

# man vsftpd
[...]
# vsftpd -v
vsftpd: version 2.3.2

Since we don't have a configuration file in place yet, we'll create a minimal one to start up the server for a test drive.

# cat > /etc/vsftpd.conf << EOF
> listen=YES
> background=YES
> anonymous_enable=NO
> local_enable=YES
> secure_chroot_dir=/var/empty
> EOF
# vsftpd
# ftp
ftp> open localhost
Trying ::1...
ftp: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to perf.ection.se.
220 (vsFTPd 2.3.2)
Name (localhost:stef):
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
150 Here comes the directory listing.
drwxr-xr-x    2 1000     20            512 Dec 11 21:40 bin
drwxr-xr-x    8 1000     20            512 Jul 19 21:50 devel
drwx------    2 1000     20            512 Aug 17 12:46 mail
-rw-------    1 1000     20         848753 Jan 06 00:48 mbox
drwxr-xr-x   13 1000     20            512 Jan 03 21:40 mydocs
-rw-r--r--    1 1000     20           3403 Jan 05 20:53 notes
drwxr-xr-x    9 1000     20            512 Sep 11 10:32 proj
drwxr-xr-x    4 1000     20            512 Jul 19 22:14 rc
drwxr-xr-x    3 1000     20            512 Nov 23 09:10 src
drwxr-xr-x    2 1000     20            512 Jan 05 20:20 tmp
226 Directory send OK.
ftp> get notes
local: notes remote: notes
150 Opening BINARY mode data connection for notes (3403 bytes).
100% |********************************************|  3403       00:00
226 Transfer complete.
3403 bytes received in 0.00 seconds (8.36 MB/s)
ftp> quit
221 Goodbye.
# rm notes

Cool, it worked fine. Let's move on. Remember to kill the server:

# # ps aux | grep vsftpd
root     20179  0.0  0.2   524   896 ??  Is     2:26PM    0:00.00 vsftpd
# kill 20179

Configuration

Everything in vsftpd is configured in the configuration file. There are no command line options or arguments and no environment variables to care about. (Actually, there is one command line option that will take a configuration statement as argument and override what's in vsftpd.conf, but that's it.) All the defaults are clearly documented in the manual too, so you are unlikely to be caught off guard by something.

A complete vsftpd.conf file is provided at the end of this HOWTO.

Basic configuration

The basic configuration covers that which both local and anonymous users share. We'll ignore the temporary configuration we used for the test-run above and start from scratch.

Run as a daemon (without the help of a inetd-style super-server), skip IPv6, run in the background and change the login banner to a generic one. (Remember to change the host name...)

listen=YES
listen_ipv6=NO
background=YES
ftpd_banner=perf.ection.se FTP server ready

Use _vsftpd as the unpriviliged user and use /var/empty as the secure chroot directory.

nopriv_user=_vsftpd
secure_chroot_dir=/var/empty

Define a few thresholds. Whether they are sensible or not depends on your hardware, the server's other duties and your requirements.

max_clients=20
max_per_ip=10
local_max_rate=0
anon_max_rate=0

Enable passive mode and define which ports that should be used for the incoming data channel connection. These are the ports you need to open in your firewall to let users connect to them.

pasv_enable=YES
pasv_min_port=40000
pasv_max_port=41000

Only accept users listed in the file /etc/ftpusers.allow to log in. Everyone else is denied before they get to enter the password. This is to prevent the passwords from being sent in the clear. Note that the classic /etc/ftpusers is a blacklist rather than a whitelist like in this case.

userlist_enable=YES
userlist_deny=NO
userlist_file=/etc/ftpusers.allow

Enable SSL/TLS, give location of the server certificate and key, only enable TLSv1 and force local users to connect using it. We will create and install the certificates later on in this document.

ssl_enable=YES
debug_ssl=NO
rsa_cert_file=/etc/ssl/perf.ection.se.crt
rsa_private_key_file=/etc/ssl/private/perf.ection.se.key
ssl_sslv2=NO
ssl_sslv3=NO
ssl_tlsv1=YES
force_anon_logins_ssl=NO
force_anon_data_ssl=NO
force_local_logins_ssl=YES
force_local_data_ssl=YES

We log to two files. To (1) /var/log/vsftpd.log via syslog (we'll modify /etc/syslog.conf later) and to (2) /var/log/xferlog without going through syslog. This to not confuse automatic log analysers that expect WU-FTPD-style logs in xferlog. Set log_ftp_protocol to YES if you want to log all commands and responses to syslog as well. That way, you can follow which directories the user checked out and possibly determine what client she used. It's great for troubleshooting but the log output will be at least ten times as large though.

syslog_enable=YES
dual_log_enable=YES
xferlog_file=/var/log/xferlog
log_ftp_protocol=NO

Enable anonymous and local access. Modify according to your needs.

local_enable=YES
anonymous_enable=YES

Local user access

Make sure users are chrooted to their home directories (regardless if they're in a “chroot list” or not), that their shell is checked for validity and that files they upload are umasked properly.

chroot_local_user=YES
chroot_list_enable=NO
check_shell=YES
local_umask=0077

Enable writing (upload and deletion) for local users.

write_enable=YES

Anonymous user access

Make vsftpd use the account ftp for anonymous access. Make anonymously uploaded files owned by the ftp account. Even if anonymous uploads are disallowed it's just not a good idea to let random users create files owned by the root user.

ftp_username=ftp
anon_upload_enable=NO
anon_umask=0777
anon_world_readable_only=YES
chown_uploads=YES
chown_username=ftp

If you want to give anonymous users the permission to upload files you should set anon_upload_enable to YES and create an upload directory in /pub to where the ftp user has write permission. The fact that anonymous users are only able to read world-readable files and the default umask of 0777 means that one user cannot download files uploaded by someone else.

# install -d -m 770 -o root -g ftp /pub/upload

Post-configuration

Although we're finished with the main vsftpd configuration there are a few things left to tinker with to make vsftpd fit properly with our OpenBSD system.

Files in /etc

We might want to give some local user access via FTP to her home directory but not shell access. We can solve this by setting her default shell to /sbin/nologin but since we have configured check_shell=YES, this “shell” must be added to /etc/shells.

# echo /sbin/nologin >> /etc/shells

Add /etc/vsftpd.conf and /etc/ftpusers.allow to /etc/changelist so that they are included in the intergrity checking of OpenBSD's daily security script.

# echo /etc/vsftpd.conf >> /etc/changelist
# echo /etc/ftpusers.allow >> /etc/changelist

Add our newly created _vsftpd user to /etc/ftpusers. We don't use it at all in our vsftpd setup but other daemons might and we don't want our unprivileged user to end up being able to log in.

# echo _vsftpd >> /etc/ftpusers

We do, however, use the /etc/ftpusers.allow file and we need to populate it with the names of the local users that should be able to use the service.

# echo "# users allowed to log in to vsftpd" > /etc/ftpusers.allow
# echo ftp >> /etc/ftpusers.allow
# echo anonymous >> /etc/ftpusers.allow
# echo stef >> /etc/ftpusers.allow

Make sure that the ftp facility is redirected to /var/log/vsftpd.log in /etc/syslog.conf like the following:

ftp.info        /var/log/vsftpd.log

Add the following to /etc/newsyslog.conf so that /var/log/vsftpd.log is rotated in the same way as /var/log/xferlog. You might wanna tweak the count, size and when values according to your situation though.

# logfile_name       owner:group  mode count size  when  flags
/var/log/vsftpd.log               600  7     250   *     Z

Lastly, make sure that the built-in ftpd is disabled.

$ grep ftpd /etc/rc.conf*
/etc/rc.conf:# Set to NO if ftpd is running out of inetd
/etc/rc.conf:ftpd_flags=NO              # for non-inetd use: "-D"
$ grep ftpd /etc/inetd.conf
#ftp            stream  tcp     nowait  root    /usr/libexec/ftpd       ftpd -US
#ftp            stream  tcp6    nowait  root    /usr/libexec/ftpd       ftpd -US
#tftp           dgram   udp     wait    root    /usr/libexec/tftpd      tftpd -s /tftpboot
#tftp           dgram   udp6    wait    root    /usr/libexec/tftpd      tftpd -s /tftpboot

Then, to make vsftpd start automatically at boot we need to give it an entry in /etc/rc.local. Make sure to put it above the final echo '.' command:

if [ -x /usr/local/sbin/vsftpd ]; then
    echo -n " vsftpd"
    /usr/local/sbin/vsftpd
fi

Generating certificates

There are two options for creating certificates for our service. (1) Creating a certificate signing request (CSR) to be sent to a proper certificate authority (CA) like Comodo or VeriSign. Or, (2) creating a signing request signed with its own key; a self-signed certificate. We'll do the latter and we'll use the openssl command. Make sure you get the hostname right everywhere or the FTP client will complain about the mismatch.

$ openssl req -new -newkey rsa:2048 -nodes -subj '/CN=perf.ection.se' \
> -keyout perf.ection.se.key -out perf.ection.se.csr
Generating a 2048 bit RSA private key
.+++
..........................................................+++
writing new private key to 'perf.ection.se.key'
-----

Now we can sign our CSR with our corresponding key to create the certificate. If you are going to send it to a CA for signing you'll need a more accurate subject than just the common name (CN).

$ openssl x509 -req -days 365 -in perf.ection.se.csr \
> -signkey perf.ection.se.key -out perf.ection.se.crt
Signature ok
subject=/CN=perf.ection.se
Getting Private key
$ ls -l
total 12
-rw-r--r--  1 stef  staff   989B Jan  8 22:16 perf.ection.se.crt
-rw-r--r--  1 stef  staff   899B Jan  8 22:16 perf.ection.se.csr
-rw-r--r--  1 stef  staff   1.6K Jan  8 22:16 perf.ection.se.key
$ openssl x509 -in perf.ection.se.crt -noout -fingerprint
SHA1 Fingerprint=AE:EA:78:12:5B:A2:D2:1A:72:70:A9:C4:F8:9C:ED:DE:A0:92:F5:35

The fingerprint is a string you can give to your users so that they can verify that they are connecting to the correct server. They will only need this the first time if they choose to trust the certificate signer.

The certificate perf.ection.se.crt should be placed in the directory /etc/ssl and the key perf.ection.se.key in the sub-directory private. Make sure to chgrp both files so that the ssl group we created during the preparation earlier can read them.

# chown root:ssl perf.ection.se.*
# chmod 444 perf.ection.se.crt 
# chmod 440 perf.ection.se.key 
# mv perf.ection.se.crt /etc/ssl
# mv perf.ection.se.key /etc/ssl/private/
# chown root:ssl /etc/ssl/private/
# chmod 750 /etc/ssl/private/

vsftpd.conf

# Run as a daemon (without the help of a inetd-style super-server), skip IPv6,
# run in the background and change the login banner to a generic one. (Remember
# to change the host name...)
listen=YES
listen_ipv6=NO
background=YES
ftpd_banner=perf.ection.se FTP server ready

# Use _vsftpd as the unpriviliged user and use /var/empty as the secure chroot
# directory.
nopriv_user=_vsftpd
secure_chroot_dir=/var/empty

# Define a few thresholds. Whether they are sensible or not depends on your
# hardware, the server's other duties and your requirements.
max_clients=20
max_per_ip=10
local_max_rate=0
anon_max_rate=0

# Enable passive mode and define which ports that should be used for the
# incoming data channel connection.
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=41000

# Only accept users listed in the file /etc/ftpusers.allow to log in. Everyone
# else is denied before they get to enter the password. This is to prevent the
# passwords from being sent in the clear. Note that the classic /etc/ftpusers
# is a blacklist rather than a whitelist like in this case.
userlist_enable=YES
userlist_deny=NO
userlist_file=/etc/ftpusers.allow

# Enable SSL/TLS, give location of the server certificate and key, only enable
# TLSv1 and force local users to connect using it.
ssl_enable=YES
debug_ssl=NO
rsa_cert_file=/etc/ssl/perf.ection.se.crt
rsa_private_key_file=/etc/ssl/private/perf.ection.se.key
ssl_sslv2=NO
ssl_sslv3=NO
ssl_tlsv1=YES
force_anon_logins_ssl=NO
force_anon_data_ssl=NO
force_local_logins_ssl=YES
force_local_data_ssl=YES

# We log to two files. To (1) /var/log/vsftpd.log via syslog (we'll modify
# /etc/syslog.conf later) and to (2) /var/log/xferlog without going through
# syslog. This to not confuse automatic log analysers that expect WU-FTPD-style
# logs in xferlog. Set log_ftp_protocol to YES if you want to log all commands
# and responses to syslog as well. That way, you can follow which directories
# the user checked out and possibly determine what client she used. The log
# output will be at least ten times as large though.
syslog_enable=YES
dual_log_enable=YES
xferlog_file=/var/log/xferlog
log_ftp_protocol=NO

# Enable anonymous and local access. Comment out according to your needs.
local_enable=YES
anonymous_enable=YES

# Make sure users are chrooted to their home directories (regardless if they're
# in a "chroot list" or not), that their shell is checked for validity and that
# files they upload are umasked properly.
chroot_local_user=YES
chroot_list_enable=NO
check_shell=YES
local_umask=0077

# Enable writing (upload and deletion) for local users.
write_enable=YES

# Make vsftpd use the account ftp for anonymous access. Make anonymously
# uploaded files owned by the ftp account. Even if anonymous uploads are
# disallowed it's just not a good idea to let random users create files owned
# by the root user.
ftp_username=ftp
anon_upload_enable=NO
anon_umask=0777
anon_world_readable_only=YES
chown_uploads=YES
chown_username=ftp

Testing

Now that you're finished there are a few test cases to consider. Anonymous users should end up in /pub, be unable to leave that directory, only read files that are world-readable and not be able to upload any files. Local users will be forced to use TLS/SSL (negotiated in-line on port 21/tcp) and be limited to their home directory where they are able to upload files. Local users are only allowed if they're listed in /etc/ftpusers.allow. All access will be logged to vsftpd.log and xferlog in /var/log.

When troubleshooting, it helps to set debug_ssl=YES and log_ftp_protocol=YES in vsftpd.conf. Also, if you are using lftp for testing, remember the -d option to output debug information.

$ lftp ftp://perf.ection.se/
cd ok, cwd=/
lftp perf.ection.se:/> ls
-rw-r--r--    1 0        0               0 Jan 08 22:42 testfile
drwxrwxr-x    2 0        503           512 Jan 08 22:44 upload
lftp perf.ection.se:/> quit
$ lftp ftp://stef@perf.ection.se/
Password: 
cd ok, cwd=/                                     
lftp stef@perf.ection.se:/> ls
drwxr-xr-x    2 1000     20            512 Dec 11 21:40 bin
drwxr-xr-x    8 1000     20            512 Jul 19 21:50 devel
drwx------    2 1000     20            512 Aug 17 12:46 mail
-rw-------    1 1000     20         875195 Jan 08 21:40 mbox
drwxr-xr-x   13 1000     20            512 Jan 03 21:40 mydocs
-rw-r--r--    1 1000     20           3403 Jan 05 20:53 notes
drwxr-xr-x    9 1000     20            512 Sep 11 10:32 proj
drwxr-xr-x    4 1000     20            512 Jul 19 22:14 rc
drwxr-xr-x    3 1000     20            512 Nov 23 09:10 src
drwxr-xr-x    2 1000     20            512 Jan 08 22:56 tmp
lftp stef@perf.ection.se:/>

That should be it. Enjoy your FTP service. Please let me know if you encounter any errors in this document.