Enthusiasm never stops

Secure chroot() remote file access via SFTP and SSH


If you want to replace the buggy and not-encrypted FTP protocol, and get rid of the FTP daemon on your system, the SFTP protocol comes to the rescue. Note that the SFTP protocol is something more than the SCP protocol (Secure copy), as it provides resuming interrupted transfers, directory listings, and remote file removal. This makes it more similar to the FTPS protocol (FTP over SSL) with the difference that it doesn’t require a separate FTP daemon, because the SSH daemon supports SFTP, which simplifies your network setup and lowers maintenance costs.

A standard security feature of the FTP servers is that logged in users are placed in a chroot jail directory, which restricts users from viewing and manipulating any other files but their own ones. Fortunately, the OpenSSH daemon supports chroot() too — see the sshd_config(5) man page.

Now that I’ve convinced you that SFTP is the right way to go for secure file transfers and remote file mounts, let’s see how we can configure it on a Debian or Ubuntu based server. All SFTP users will be kept in a root chroot() directory:

mkdir -m 751 /home/sftp-chroot /home/sftp-chroot/{dev,home}

The man page says that all components of the pathname must be root-owned directories that are not writable by any other user or group. Let’s verify this before going further:

ls -lad / /home /home/sftp-chroot /home/sftp-chroot/home
drwxr-xr-x 23 root root 4096 2011-01-31 17:15 /
drwxr-x--x  9 root root 4096 2011-01-31 18:05 /home
drwxr-x--x  4 root root 4096 2011-01-31 17:38 /home/sftp-chroot
drwxr-x--x  3 root root 4096 2011-02-01 08:46 /home/sftp-chroot/home

SFTP users’ home directories will be placed in “/home/sftp-chroot/home/$SFTPUSER” (after the chroot() the actual directory would be “/home/$SFTPUSER”). Note that this still gives full privacy, because users can’t list the root chroot() directory “/” nor the root home directory “/home”, as their permissions are 0751. Furthermore, even if one user guessed the username and home directory of another user, they cannot enter their home directory, because the users’ home directories give no permissions for the group “others”. The commands for managing users’ home directories are at the very end of this article.

In order for the chroot()’ed users and processes to be able to do logging, a listening UNIX socket of syslog must be created inside the root chroot() directory:

cat > /etc/rsyslog.d/sftp-chroot.conf <<'EOF' # the single quotes are important
# the following syslog socket will be used by all chrooted users
$AddUnixListenSocket /home/sftp-chroot/dev/log

# Log internal-sftp activity in a separate file
:programname, isequal, "internal-sftp" -/var/log/sftp.log
:programname, isequal, "internal-sftp" ~

/etc/init.d/rsyslog restart

We verify that the syslog UNIX socket was created:

ls -la /home/sftp-chroot/dev/log
srw-rw-rw- 1 root root 0 2011-01-31 16:42 /home/sftp-chroot/dev/log

We will also rotate the log files weekly:

cat > /etc/logrotate.d/sftp-chroot <<'EOF'
/var/log/sftp.log {
  rotate 52
    invoke-rc.d rsyslog reload > /dev/null

Finally, let’s set up the OpenSSH daemon accordingly:

# disable the standard "sftp" subsystem in the sshd config
perl -pi -e 's/^(\s*Subsystem\s+sftp\s+)/#$1/i' /etc/ssh/sshd_config

# enable the chroot "sftp" subsystem
cat >> /etc/ssh/sshd_config <<'EOF'

Subsystem sftp internal-sftp

Match group sftponly
  ChrootDirectory /home/sftp-chroot
  ForceCommand internal-sftp -l VERBOSE
  AllowTcpForwarding no
  PermitRootLogin no
  X11Forwarding no
  AllowAgentForwarding no
  # Change to yes to enable tunnelled clear text passwords, not recommended
  PasswordAuthentication no

groupadd sftponly
/etc/init.d/ssh restart

A few notes about the OpenSSH configuration:

  • This configuration requires OpenSSH 5.2 or later — discussion about this in the reference links below.
  • The SCP protocol is not supported, at least not very easily. If you really need SCP, you’d need to re-create a full binary environment, in order to provide standard SSH access in a chroot() way. This is not discussed here, nor tested by me. You are encouraged to use SFTP instead. [source article]
  • Some users report that the time-stamp of the messages in the syslog log file were wrong. This is indeed possible since there is no /etc/timezone file in the root chroot() directory. I haven’t encountered this bug, as my system is in GMT+0, or the bug no longer exists. [source article]

In the end, let’s take a look at Users management (sample Bash script below in the comments).

Adding an SFTP user:

export SFTPUSER='testsftp1'
useradd -d "/home/$SFTPUSER" --groups sftponly -s /bin/false -p '!' "$SFTPUSER"
passwd "$SFTPUSER" # if you enabled tunnelled clear text passwords, regardless of the security implications
mkdir -m 770 "/home/sftp-chroot/home/$SFTPUSER"
chown root:"$SFTPUSER" "/home/sftp-chroot/home/$SFTPUSER"

Enabling public/private key authentication, the more secure choice, for an SFTP user:

export SFTPUSER='testsftp1'
mkdir -m 750 "/home/$SFTPUSER" "/home/$SFTPUSER/.ssh"
chown root:"$SFTPUSER" "/home/$SFTPUSER" "/home/$SFTPUSER/.ssh"
vi "/home/$SFTPUSER/.ssh/authorized_keys" # add the public key

Deleting an SFTP user and its data:

export SFTPUSER='testsftp1'
userdel "$SFTPUSER"
rm -r "/home/sftp-chroot/home/$SFTPUSER"
rm -r "/home/$SFTPUSER" # if you authenticate using public keys

The end-result of all those efforts is that when an SFTP user logs in to your system, they see only their home directory and nothing else. This is because after the chroot, sshd(8) changes the working directory to the user’s home directory. Furthermore the user cannot list nor enter other home directories in the root chroot() directory, because of the directory setup we created.

Here is a sample log from the SFTP syslog file, click “show source” to view it:

Jan 31 17:55:10 m1 internal-sftp[13237]: session opened for local user testsftp1 from []
Jan 31 17:55:10 m1 internal-sftp[13237]: received client version 3
Jan 31 17:55:10 m1 internal-sftp[13237]: realpath "."
Jan 31 17:55:12 m1 internal-sftp[13237]: open "/home/testsftp1/fb.php" flags WRITE,CREATE,TRUNCATE mode 0644
Jan 31 17:55:13 m1 internal-sftp[13237]: close "/home/testsftp1/fb.php" bytes read 0 written 735
Jan 31 18:04:14 m1 internal-sftp[13237]: session closed for local user testsftp1 from []


Author: Ivan Zahariev

An experienced Linux & IT enthusiast, Engineer by heart, Systems architect & developer.

9 thoughts on “Secure chroot() remote file access via SFTP and SSH

  1. Thanks for the comprehensive article – some of the other posts floating around have slightly incorrect config settings.

    As another note, I wanted my internal-sftp uploads to have group write permissions. The ForceCommand appears to support a -u flag for umask in newer OpenSSH versions (I’m on 5.5p1). My ForceCommand looks like this:

    ForceCommand internal-sftp -l VERBOSE -u 0002

  2. hello, thanks for the tip, it works great.
    Users have access SFTP, rights are ok!

    But users cant log into ssh (using password), i get these error :

    Accepted password for alex from XXX port 54588 ssh2
    Aug 15 18:35:24 srv1 sshd[15532]: pam_unix(sshd:session): session opened for user alex by (uid=0)
    Aug 15 18:35:24 srv1 sshd[15536]: error: /dev/pts/1: No such file or directory
    Aug 15 18:35:24 srv1 sshd[15536]: error: open /dev/tty failed – could not set controlling tty: No such file or directory
    Aug 15 18:35:24 srv1 sshd[15532]: pam_unix(sshd:session): session closed for user alex

    • Hi, thanks. I’m not sure what you want to achieve for such chroot()’ed users when they log in via an interactive SSH session:
      * If you expect that they see only their “/home/$user” directory, and nothing else, then this makes no sense, as they will not be able to execute any commands. Even the simple “/bin/ls” won’t be there, as the “/bin” directory is hidden behind the chroot()’ed environment.
      * If you expect that the users see only their “/home/$user” directory AND the standard directories, then you have to re-create these directories. The easiest way would be to bind mount them read-only in the “/home/sftp-chroot” directory structure, but this needs a lot of testing, and in the end you’ll probably end up by exporting most of your existing root directories. So it won’t make any difference to a standard non-chroot()’ed environment.

      The OpenSSH maintainers have said it clearly:
      “Unfortunately, setting up a chroot(2) environment is complicated, fragile and annoying to maintain. The most frequent reason our users have given when asking for chroot support in sshd is so they can set up file servers that limit semi-trusted users to be able to access certain files only. Because of this, we have made this particular case very easy to configure.”

      In a nutshell: Don’t mix chroot()’ed SFTP and interactive shell users in one configuration. If you need a shell for the same home directories, then I (blindly) suggest that you create duplicate users, with different usernames, but with home directories set to “/home/sftp-chroot/home/$SFTPUSER”, and User ID + Group ID same as the original “$SFTPUSER”. This way when those standard SSH users log in to the system, they will be placed in “/home/sftp-chroot/home/$SFTPUSER”. Though in this case, I don’t see why you needed the chroot()’ed environment in first place…

      I hope I was able to made myself clear. Good luck!

  3. Hi, thank you for these explanations.

    By my lack of adequate knowledge in English (french) and by the way in Linux, i misunderstood your tutorial…but with your precedent explanations, i fully understand the mechanism and why it cant work like this.

    So, there is no “magic” solution to allow SFTP access AND ssh access ? (i mean, no need to put my hands dirty).

    Last (big?) question, if you were web hoster, what cleanest solution would you use ?

    I hope you will understand me! 🙂

    Thanks for all.

    Romain P.

    • If you want to allow both SFTP and SSH access, then I won’t use chroot(). Instead, I’ll make the “/home/$user” directories with strict permissions, disallowing the “other” to be able to chdir() there. For example, for user “test”, I’d make “/home/test” owned by “root:test”, and permissions 0770, so that the group “test” (and thus user “test”) have full permissions over the directory, but the owner of the directory is still “root”, so that “test” cannot rename the directory to something else. Since you mentioned a web hosting environment, you’d also probably need to allow the web server to access the directory too. In this case, I’d use the Linux ACL file-system extensions, and grant “x” permissions for the group of the web server.

  4. awsome info, i did want to ask.. i have a user created and i wanted to make their chroot directory /var/www/html

    but of course when i do this i am told they dont have permission.

    i just can get it to work… what am i doing wrong or how can i allows an SFTP user access to /var/www/html directory only?

    • I can’t really comment, because I don’t have enough information about your setup, like all the directories owners/permissions, etc. Though I have a quick suggestion for you which you may try as a workaround:

      Let’s assume that your user is named “joe”. First make sure that in a regular environment, “joe” can access the files in “/var/www/html” — login via SSH and make “cd /var/www/html”. Until you’ve fixed this, don’t try the chroot() environment via SFTP

      After that, you can bind-mount the “/var/www/html” directory into the chroot() home directory of “joe”. Bind-mounting is like hard-links but works for directories. Example commands, run as “root”:

      mkdir /home/sftp-chroot/home/$SFTPUSER/html
      mount -o bind /var/www/html /home/sftp-chroot/home/$SFTPUSER/html

      Once you’ve executed the above commands, when you enter “/home/sftp-chroot/home/joe/html”, you will see the content of “/var/www/html”. Modifying files in any of the directories will be reflected in both of them. More info about bind-mounting can be found in the man page of mount(8).

  5. Hi,
    you have made awesome manual. My small contribution to your guide is a Bash script I wrote for easier user management.

    function user_add() {
            export SFTPUSER="${1}"
            useradd -d "/home/$SFTPUSER" --groups sftpusers -s /bin/false -p '!' "$SFTPUSER"
            passwd "$SFTPUSER"
            mkdir -m 770 "/home/sftpusers/home/$SFTPUSER"
            chown root:"$SFTPUSER" "/home/sftpusers/home/$SFTPUSER"
            echo "User ${1} added successful."
    function user_del() {
            export SFTPUSER="${1}"
            userdel "$SFTPUSER"
            rm -r "/home/sftpusers/home/$SFTPUSER"
            rm -r "/home/$SFTPUSER"
            echo "User ${1} deleted successful."
    function key_add() {
            export SFTPUSER="${1}"
            mkdir -m 750 "/home/$SFTPUSER" "/home/$SFTPUSER/.ssh"
            chown root:"$SFTPUSER" "/home/$SFTPUSER" "/home/$SFTPUSER/.ssh"
            vi "/home/$SFTPUSER/.ssh/authorized_keys"
    echo -n -e "33[4ma33[0mdd user, 33[4md33[0melete user or 33[4mq33[0muit? "
    read action
    if [ -z "$action" ]
            echo -n "Parameter missing."
            exit 1
    elif [ "$action" = "a" ]
            echo -n "Username: "
            read username
            if [ -z "$username" ]
                    echo "Parameter missing. Did you forget the username?"
                    exit 1
                    user_add $username
            echo -n -e "\nDo you want to add key? (y/n) "
            read key
            if [ "$key" = "y" ]
                    key_add $username
                    exit 0
    elif [ "$action" = "d" ]
            echo -n "Username (l for list): "
            read username
            if [ -z "$username" ]
                    echo "Parameter missing. Did you forget the username?"
                    exit 1
            elif [ $username = "l" ]
                    echo -e "\nUsers:"
                    awk -v LIMIT=500 -F: '($3&gt;=LIMIT) &amp;&amp; ($3!=65534)' /etc/passwd | cut -d: -f1
                    echo -n -e "\nUsername: "
                    read username
                    user_del $username
                    user_del $username
    elif [ "$action" = "q" ]
            exit 0
            echo "Oops! Something went wrong."
            exit 1

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s