![]() |
| December 2002 | Get BSD | New to BSD? | Search BSD | Submit News | FAQ | Contact Us | Join Us |
|
Often, you find yourself in need of exchanging files with people you know. You may need to get a core dump from a user of your software, or create a file repository which the members of a small mailing-list can read and write to. These are the cases where FTP works best. FTP easily handles big files, so it's an easy way to get around restrictive email quotas. It is also widely supported, so it will not require you to give live telephone support to a relative who doesn't have, for example, SSH installed by default on his or her system. And even if you're willing to put in this kind of effort, your colleague might be employed by someone who doesn't trust the employees enough to let them install software on their desktops. For all of these cases, it's nice to able to say "just point your browser to this location", and as luck has it, FTP lets you do exactly that.
FTP also has its weaknesses. The biggest one is using a clear-text protocol that exposes passwords and data to sniffers along the way. I will not address that problem here. Instead, what I'll attempt to do is reduce its impact by making sure the passwords are good for nothing but FTP access. We will also look at limiting FTP access such that even the password holder - legitimate or otherwise - can do very little except what the the account was designed for.
We will do this by installing an FTP dæmon in a jailed environment. The jail(8) command lets you create a virtual machine, so to speak, inside a host. This virtual machine is severely limited in the ways it can interact with the host. For most purposes, it's nothing more than an isolated machine that can talk to the host only via IP networking. It is secure enough to let people you don't otherwise trust have the root password to a jailed system. In fact, there are hosting companies that do exactly that - they sell access to a shared machine, where each client gets root access to a virtual machine inside it. Even though they each have full privileges on their own machine, clients are still well-separated from each other.
But our approach is a more lightweight one. We will have exactly one process inside the jail - the FTP dæmon. It will have its own directory hierarchy, including password files. So when you create new FTP users, there's no need to worry about the access they have to your system. Those accounts will only work inside the jailed system. And even if there is a vulnerability in the FTP dæmon, the intruder can do very little damage. It will also be lightweight in another sense - it will not require us to allocate a routable IP address for the jail.
The FTP dæmon I use in the examples is proftpd. I chose it because it supports fine-grained control over file access. In particular it lets you restrict uploads and downloads by remote host IP address. But the same technique would work with other FTP dæmons.
We start by installing proftpd from ports:
cd /usr/ports/ftp/proftpd && make install clean
Note that this installs everything in the usual places - we don't have any jails yet. We will start locking it up in a moment. For that we need a good site for the jail, meaning a root directory from which to hang the whole FTP-related hierarchy. Our site will be /usr/j1, which can be just a directory we create for this purpose. A better solution, though, is to have a dedicated filesystem for this and mount it on /usr/j1. This way you can guarantee that attackers or innocent user mistakes don't end up filling your system disk - at worst, they'll fill up the dedicated filesystem.
Obviously we need the proftpd executable in the jail. Copy it in:
tgt=/usr/j1 mkdir -p $tgt/usr/local/libexec install -C /usr/local/libexec/proftpd $tgt/usr/local/libexec
What else? We also need the dynamic libraries that proftpd is linked against. Remember that once we start up the jail, the process can't see anything that isn't included in the hierarchy (that is, in our example, the one rooted at /usr/j1). This means we have to copy all of the necessary files over.
The easy way to find all the required libraries is with the ldd(1) command. After using it, we arrive at the following sequence to set things up:
mkdir -p $tgt/usr/lib install -C /usr/lib/libcrypt.so.2 /usr/lib/libutil.so.3 /usr/lib/libwrap.so.3 \ /usr/lib/libpam.so.1 /usr/lib/libc.so.4 $tgt/usr/lib ln -s libcrypt.so.2 $tgt/usr/lib/libcrypt.so ln -s libutil.so.3 $tgt/usr/lib/libutil.so ln -s libwrap.so.3 $tgt/usr/lib/libwrap.so ln -s libpam.so.1 $tgt/usr/lib/libpam.so ln -s libc.so.4 $tgt/usr/lib/libc.so
That should be enough, shouldn't it? Let's try and start up our FTP dæmon. For now we won't set it up in a maximum-security jail yet. Instead we will try to cage it in a chroot(8) jail, which is easier to set up and will help us debug things.
bash-2.05a# chroot $tgt /usr/local/libexec/proftpd - Fatal: unable to read configuration file '/usr/local/etc/proftpd.conf'
Oops, it looks like we forgot to install the configuration file in the jail. The proftpd port comes with a default configuration file which is very close to what we need. We copy that file over, then modify it to our needs:
mkdir -p $tgt/usr/local/etc cp /usr/local/etc/proftpd.conf $tgt/usr/local/etc/proftpd.conf vi $tgt/usr/local/etc/proftpd.conf
The first modification is to make sure users can only see their own home directories. Another change we make prevents the users from modifying the access-list by way of uploading a .ftpaccess file to their directory. proftpd will read that file to determine access, so we can't allow users to change it. This problem doesn't exist in standard configurations where users are assumed to have shell access. Users who have shell access can simply edit any of their files. But our jailed environment restricts shell access, and so we plug the last hole through which they can possibly modify the access file.
Here is a summary of the additions we're making to the proftdp.conf file:
# # limit all users to their $HOME # DefaultRoot ~ # Only allow sane characters in stored file names, and they must start with letter/digit PathAllowFilter ^[A-Za-z0-9][A-Za-z0-9._-]*$
Next, we make the jail directory hierarchy look more like the standard one the FTP dæmon expects to find. This involves creating directories and installing helper utilities as well as various configuration files:
mkdir -p $tgt/usr/local/bin install -C /usr/local/bin/ftpwho /usr/local/bin/ftpcount $tgt/usr/local/bin mkdir -p $tgt/usr/local/sbin install -C /usr/local/sbin/ftpshut $tgt/usr/local/sbin mkdir -p $tgt/var/log touch $tgt/var/log/xferlog touch $tgt/var/log/wtmp mkdir -p $tgt/var/run/ mkdir -p $tgt/usr/local/etc mkdir -p $tgt/etc install -C /etc/localtime /etc/host.conf /etc/hosts /etc/resolv.conf /etc/auth.conf $tgt/etc fgrep nobody /etc/master.passwd > master.passwd chmod 600 $tgt/etc/master.passwd fgrep nogroup /etc/group > $tgt/etc/group pwd_mkdb -p -d $tgt/etc/ $tgt/etc/master.passwd pw -V $tgt/etc useradd root -c "Charlie &" -u 0 -d /nonexistent -s /sbin/nologin
At this point you'll get a warning: "unknown root shell". This is fine - we don't intend to log in as root into the jail anyway. This completes the setup for the jail. But before we activate it we still need to make sure it can connect to the outside world.
If you look up the jail(8) man page, you'll see that you need to give it an IP address for the virtual machine to use. This is how external hosts will talk to it, so there's no easy way to avoid using one. But public IP addresses are hard to come by - and a public address is precisely what we need to make our FTP server dæmon useful. What we'd like is to avoid buying an additional IP address just for the FTP server. After all, our host already has a public IP address by which it's already known. What we want is some mechanism to help us hide the actual address that the virtual machine may have, and make the outside world believe that the virtual machine is using the public address. Does this sound familiar? Of course it does. This is exactly what the NAT mechanism does. Traditionally, it is used to hide a pool of machines on an internal network and connect them to a public network using just a single address. But we can use it just the same on a single physical machine, letting it hide a virtual machine. When a FTP request comes in to the host, it will translate the address so it goes to the virtual machine. If the virtual machine needs to establish an outgoing connection, it can do the translation in the outgoing direction just as well.
Note that natd(8) is integrated into FreeBSD's ipfw(8) firewall. So we have to build a kernel with ipfw support. However, we won't be using it for IP filtering - it will basically allow anything through. If you want to increase security, you can certainly extend the ipfw ruleset appropriately.
To use natd, we add this to the kernel config file:
options IPFIREWALL options IPDIVERT
and recompile the kernel. We also need to enable NAT. Add all of these lines to /etc/rc.conf. Replace "fxp0" with the name of your external interface:
natd_enable="YES" natd_flags="-f /etc/natd.conf" natd_interface="fxp0" ifconfig_fxp0_alias0="inet 10.0.1.1/32" gateway_enable="YES" firewall_enable="YES" firewall_type="OPEN" firewall_quiet="YES" syslogd_flags="-s -l /usr/j1/var/run/log"
Note that we committed to the IP address 10.0.1.1 for our FTP virtual machine. But it doesn't really matter what it is - no external host will ever see it. We continue to configure natd. Create /etc/natd.conf and populate it with the following lines:
use_sockets yes same_ports yes unregistered_only yes redirect_port tcp 10.0.1.1:21 21
Almost done! The last touch makes sure the dæmon doesn't identify itself with the non-routable address 10.0.1.1 to visitors. An easy way to achieve this is to add an entry in the jailed /etc/hosts file, like this:
echo "10.0.1.1 ftp.mydomain.example.com" >> $tgt/etc/hosts
where ftp.mydomain.example.com stands for the public name of your host.
The next task is to create a user account in the jail. I simply use the pw(8) utility for this. If you tell it to modify the password files inside the jail, the new user will have FTP access but will not otherwise be recognized by the host. In particular, they can't use the account for SSH access. This also means that the remote user has no way to change the password once we set it. So we might as well choose a good password for them. I like using the "apg" utility (from the ports), that creates a pseudo-random password while trying to keep it pronounceable. One way or the other, once we have the password we can create the account and the home directory:
pw -V $tgt/etc useradd joe -c "My friend" -d /data/joe -g nogroup -s /sbin/nologin -h 0 mkdir -p $tgt/data/joe
Setting permissions on the data directory can be tricky. First, if we want to
change the owner or group, we must remember that the user named "joe"
does not exist on the host system. Therefore naming him in the chown(8) command
will fail. One workaround is to use numeric identifiers (looking into
$tgt/etc/passwd to find out what they are). Another is to chroot the
"chown" command so it has to use the user mappings inside the jail,
where they are valid. For example: chroot $tgt chmod -w /data/joe
Another thorny aspect of FTP permissions is abuse potential. Remember that the password is sent in the clear. If it is ever stolen, and you have a directory that is open for both reading or writing, you can bet on getting a call from your ISP regarding stolen or pornographic materials exchange on your server. Securing FTP access is out of the scope of this article. However the one-sentence summary is: never leave a read/write directory that is globally accessible.
After deciding on an access policy, I can notify my friend that he can log in to the FTP server. If he is not familiar with old-school interfaces, I just tell him that on most browsers a URL like ftp://joe@ftp.mydomain.example.com/ would work to connect him. If you prefer, you can even include the password, like this: ftp://joe:password@ftp.mydomain.example.com/.
The proftpd port comes with a startup/shutdown script at /usr/local/etc/rc.d/proftpd.sh.sample. We use it as a starting-point.
cd /usr/local/etc/rc.d cp proftpd.sh.sample proftpd.sh chmod +x proftpd.sh vi proftpd.sh
Edit the "start)" section to read:
/bin/mkdir -p /usr/j1/var/run/proftpd if [ -x /usr/j1/usr/local/libexec/proftpd ]; then jail /usr/j1 ftp.mydomain.example.com 10.0.1.1 /usr/local/libexec/proftpd && echo -n ' proftpd'
After a reboot - in order to load the NAT-enabled kernel and all the changes to rc.conf - you will be able to start and stop the FTP jail by passing the "start" or "stop" argument to /usr/local/etc/rc.d/proftpd.sh. But as-is, it will be started automatically by the init sequence. You can disable this by making the proftpd.sh file non-executable.
It's also a good idea to make the old proftd.conf file a link to the jailed version, lest someone tries to edit it, not realizing it is not the one that is being used.
cd /usr/local/etc/ ln -fs $tgt/usr/local/etc/proftpd.conf .
We installed a jailed FTP dæmon. This in itself is not enough to secure the service. Directory and access permissions should be very carefully examined to prevent abuse, as the restrictions on .ftpaccess above demonstrated. In addition, we need to keep the installation up-to-date with security patches. Because we have libraries installed in special locations, they will not be updated in the next system upgrade. We need to remember to copy the new files over manually. A better idea is to write a script to do that and to hook it to the shutdown sequence.
This general method of installing a bare-bones jail can apply to many other services. For example, you can jail your SMTP dæmon together with a POP3 dæmon to give your friends email-only privileges. And because we don't use a public IP address, we can install more than one jailed service on the same physical host.
I would like to thank Jacob Joseph and Paul Komarek for their help in the preparation of this article.