Setting up SFTP-only users on RHEL/CentOS 5

Introduction

Normally, when a user account is created on a Unix system, the user in question can log into the system via ssh, forward TCP ports and use the SFTP subsystem if it is enabled in the server configuration. However, at some point, a sysadmin might face the need to create SFTP-only accounts for a number of users.

A naïve approach would involve creating a new user and setting his or her shell to /sbin/nologin. This solution, nevertheless, has two annoying downsides, which might ruin the security of the system, if the users are not trusted enough.

The first one is that the users would still be able to forward ports if this is globally enabled for the trusted user accounts (which includes sshd acting as a SOCKS proxy) and the second one is that the users would be able to lurk around the root file system, which certainly cannot be considered as an advantage when the users are not reliable enough.

In order to solve the first problem, one would need to apply certain configuration parameters to a specific subset of user accounts. The second issue can be avoided by instructing the SSH server to chroot into the user’s directory upon successful login.

One can conveniently apply a bunch of configuration options to specific accounts using the Match directive available in the latest versions of the OpenSSH server:

Match user badguy
    AllowTcpForwarding no
    X11Forwarding no
    ChrootDirectory /srv/sftp
    ForceCommand internal-sftp
    
Match group sftponly
    ...

However, this directive is only included in OpenSSH 5.x and above, whereas Red Hat Enterprise Linux 5 ships with OpenSSH 4.x. Therefore, an alternative approach will be outlined below.

In what concerns the chrooting, it can be achieved using the ChrootDirectory directive. Thankfully, corresponding patches have been backported to OpenSSH 4.x by Red Hat engineers.

Strictly speaking, it unnecessary to build a proper chroot for SFTP-only users, since OpenSSH includes a built-in SFTP implementation that does not depend upon any external libraries, but if one wants the users to be politely rejected when they try to connect via plain ssh, one could just make /sbin/nologin work and that is it.

N.B.: Red Hat Enterprise Linux 6 ships a newer OpenSSH version that fully supports the Match directive. This means that for RHEL 6 this tutorial would boil down to the configuration snippet for sshd_config presented above. Setting up a parallel running SFTP-only sshd instance is unnecessary. However, one might still wish to skim through the article for the advice on how to create a proper chroot and read the remarks in the Conclusion.

Implementation

Setting up a parallel SFTP-only sshd instance

Since Match directive is unavailable and the installation of extra unsupported software is to be avoided at all costs, one can bring up a parallel SFTP-only sshd instance. The proper way to go would be, of course, to create an extra RPM, i.e. openssh-sftp which installs an additional init script and symlinks, but for an one-time deployment this might be an overkill.

First, let us create an init script, which is a straightforward modification of the stock init script supplied by Red Hat:

root@box # cat > /etc/rc.d/init.d/sshd-sftponly
#!/bin/bash
#
# Init file for SFTP-only OpenSSH server daemon
#
# chkconfig: 2345 55 25
# description: SFTP-only OpenSSH server daemon
#
# processname: sshd-sftponly
# config: /etc/ssh/ssh_host_key
# config: /etc/ssh/ssh_host_key.pub
# config: /etc/ssh/ssh_random_seed
# config: /etc/ssh/sshd_config-sftponly
# pidfile: /var/run/sshd-sftponly.pid

# source function library
. /etc/rc.d/init.d/functions

RETVAL=0
prog="sshd-sftponly"

# Some functions to make the below more readable
SSHD=/usr/sbin/sshd-sftponly
PID_FILE=/var/run/sshd-sftponly.pid

# ZYV
LOCK_FILE=/var/lock/subsys/sshd-sftponly
OPTIONS=" -f /etc/ssh/sshd_config-sftponly "

runlevel=$(set -- $(runlevel); eval "echo \$$#" )

start()
{
    cp -af /etc/localtime /var/empty/sshd/etc

    echo -n $"Starting $prog: "
    $SSHD $OPTIONS && success || failure
    RETVAL=$?
    [ "$RETVAL" = 0 ] && touch $LOCK_FILE
    echo
}

stop()
{
    echo -n $"Stopping $prog: "
    if [ -n "`pidfileofproc $SSHD`" ] ; then
        killproc $SSHD
    else
        failure $"Stopping $prog"
    fi
    RETVAL=$?
    # if we are in halt or reboot runlevel kill all running sessions
    # so the TCP connections are closed cleanly
    if [ "x$runlevel" = x0 -o "x$runlevel" = x6 ] ; then
        killall $prog 2>/dev/null
    fi
    [ "$RETVAL" = 0 ] && rm -f $LOCK_FILE
    echo
}

reload()
{
    echo -n $"Reloading $prog: "
    if [ -n "`pidfileofproc $SSHD`" ] ; then
        killproc $SSHD -HUP
    else
        failure $"Reloading $prog"
    fi
    RETVAL=$?
    echo
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        stop
        start
        ;;
    reload)
        reload
        ;;
    condrestart)
        if [ -f $LOCK_FILE ] ; then
            stop
            # avoid race
            sleep 3
            start
        fi
        ;;
    status)
        status -p $PID_FILE openssh-daemon
        RETVAL=$?
        ;;
    *)
        echo $"Usage: $0 {start|stop|restart|reload|condrestart|status}"
        RETVAL=1
esac
exit $RETVAL

CTRL+D

The configuration file would look as follows (not all directives are necessary, take into account the defaults of the distribution in use):

root@box # cat > /etc/ssh/sshd_config-sftponly
# ZYV
PasswordAuthentication no
PermitRootLogin no
PidFile /var/run/sshd-sftponly.pid
Port 2234
Protocol 2
UsePAM no

Subsystem       sftp    internal-sftp

ChrootDirectory /srv/sftp
AllowTcpForwarding no
X11Forwarding no
ForceCommand internal-sftp

CTRL+D

It is important to turn off the use of PAM for authentication in which case sshd-sftponly will fall back to reading /etc/passwd and /etc/shadow, instead of using the pluggable authentication module definitions for authentication sources and methods. Otherwise, one would need to provide an additional PAM configuration file basing upon /etc/pam.d/sshd as a template for sshd-sftponly, however, it is clearly an overkill if no advanced authentication scheme (e.g. against LDAP) is required.

Then, one needs to create a link to the sshd binary, register and start the service:

root@box # ln -s /usr/sbin/sshd /usr/sbin/sshd-sftponly
root@box # chkconfig --add sshd-sftponly
root@box # service sshd-sftponly start

Creating an SFTP-only user

The next task would be to create a group for SFTP-only users and the users themselves:

root@box # groupadd sftponly
root@box # useradd badguy -g sftponly -s /sbin/nologin -m -K UMASK=0022
root@box # passwd badguy

Here -g specifies the main group, -s sets the shell, -m creates the home directory from a skeleton and -K overrides the default options with regards to umask (optional).

It is important to set a strong password for the user (which can be immediately discarded) even though the public key authentication is to be used, because otherwise the system would consider this account to be inactive.

Now it is necessary to set up the public key authentication:

badguy@foo:~$ ssh-keygen -t rsa -b 4096

root@box # mkdir /home/badguy/.ssh
root@box # chmod 700 /home/badguy/.ssh
root@box # cat > /home/badguy/.ssh/authorized_keys
...
CTRL+D

root@box # chmod 600 /home/badguy/.ssh/authorized_keys

Creating a proper chroot

In order to avoid potential privilege escalation, the chroot and all path components leading up to the chroot have to be owned and only writable by root. Additionally, it is necessary to hardlink the shell and some supporting libraries inside the chroot:

root@box # mkdir -p /srv/sftp/{home,lib,sbin}
root@box # mkdir /srv/sftp/home/badguy

root@box # chown badguy:sftponly /srv/sftp/home/badguy

root@box # ln /lib/ld-2.5.so /srv/sftp/lib
root@box # ln /lib/ld-linux.so.2 /srv/sftp/lib
root@box # ln /lib/libc-2.5.so /srv/sftp/lib
root@box # ln /lib/libc.so.6 /srv/sftp/lib
root@box # ln /sbin/nologin /srv/sftp/sbin

Luckily, the OpenSSH server will not allow to use a chroot with wrong permissions.

Wrapping up

Now it is time to set up a new host entry and try it out:

badguy@foo:~$ cat >> ~/.ssh/config

Host box
  HostName box.com
  Compression yes
  IdentityFile ~/.ssh/id_rsa
  Port 2234
  User badguy

CTRL+D

badguy@foo:~$ sftp box

Now all these amazing feats would be for nothing if one would not tell the standard sshd daemon to deny connections for SFTP-only users and it would happily let them in:

root@box # cat >> /etc/ssh/sshd_config

# ZYV
DenyGroups sftponly

CTRL+D

root@box # service sshd restart

Conclusion

There are few additional notes, that I would like to make before closing the article.

First, if you would like to test SFTP access, you need to use sftp program as opposed to scp. It came as a surprise to me (which probably reflects one of the gaps in my knowledge due to me being an autodidact), but the widely used scp program as opposed to the popular beliefs, in fact, does not normally implement the SFTP protocol.

It is indeed the case in some operating systems, but the canonical version of scp implements the BSD RCP protocol which is tunneled through the Secure Shell (SSH) protocol to provide encryption and authentication. So bear this in mind and use sftp instead.

Another point that is worth being discussed is why one needs a separate root-owned directory tree for a chroot, instead of chrooting directly into users’ home directories. This goes back to CVE-2009-2904, when it was discovered that badass users could be able to escalate their privileges via hard links to setuid programs that use configuration files within the chroot directory. In the end it was decided that chrooting in user-owned directories actually defeats the purpose of the exercise and additional checks were introduced to restrict the possible sets of permission of the potential chroots.

Enjoy and if I have missed something in my setup please do let me know!

CC BY-NC-ND