Skip to main content

chrooted SFTP

Creating chroot SFTP accounts

For $DAYJOB I had to create user accounts for customers and give them access to SFTP files to/from secured areas of our server. We wanted to use chroot functionality to ensure that no customer could see other customers' data, and prevent them from poking around potentially sensitive areas of the server. After a bit of trial-and-error, I've listed the lessons-learned here in a cook-book fashion so that in case I ever have to do it again, I have the steps documented.

This post was spurred to exist thanks to this Reddit post asking about creating an encrypted FTP server on OpenBSD so my reply there became the basis for this post.

Steps to reproduce

  1. create the "sftp users" group, which I'll refer to here as customers
    groupadd customers
    
  2. create the new user. For this example, I use "acmecorp" but I define the variable $NEWUSER and use it throughout the rest of this post:
     adduser
     NEWUSER=acmecorp
    
  3. add them to the customers group when prompted for the "other groups" or, if you have a pre-existing user, use
    usermod -Gcustomers $NEWUSER
    
  4. to put them in their own chroot we need to create a fake hierarchy in /home/$NEWUSER/ so we'll end up with /home/$NEWUSER/home/$NEWUSER/
  5. make a temporary "user" directory and create the fake /home inside that
    CHROOT="$(mktemp -d -p /home)"
    mkdir -p "${CHROOT}/home/"
    
  6. set the permissions & ownership on that fake hierarchy:
    chown -R root:wheel "$CHROOT"
    chmod -R 0755 "$CHROOT"
    
  7. move the user's old home directory under the chrooted /home
    mv -v "/home/$NEWUSER" "$CHROOT"/home/
    
  8. and then rename the chroot back to the original /home/$NEWUSER directory
    mv -v "$CHROOT" "/home/$NEWUSER"
    
  9. while a bit confusing, I found that some users expected to have $SFTP drop them in / and be able to do a relative cd home/$USER while others expect to be dropped in their $HOME so by adding a fake home/$USER that points to the right place, it allows for both of these. This might be optional, but helps me stave off customer script breakage:
    mkdir -p "/home/$NEWUSER/home/$NEWUSER/home"
    ln -s .. "/home/$NEWUSER/home/$NEWUSER/home/$NEWUSER"
    
    It might also stave off issues with the next step, since the home directory in /etc/passwd can point to /home/$NEWUSER/home/$NEWUSER regardless of whether in the chroot or not and still point to the right place.
  10. we've messed with their home directory, so update /etc/passwd to reflect where things should find the home directory now
    usermod -d "/home/$NEWUSER/home/$NEWUSER" "$NEWUSER"
    
  11. now the user is configured properly, so let sshd know how to treat members of the customers group. Edit your /etc/ssh/sshd_config to include this block at the end:
    Match Group customers
       ChrootDirectory /home/%u
       ForceCommand internal-sftp
       PermitTunnel no
       AllowTcpForwarding no
       AllowAgentForwarding no
       X11Forwarding no
    
    /etc/ssh/sshd_config
    I don't know whether ForceCommand kills off the ability to do PermitTunnel, AllowTcpForwarding, AllowAgentForwarding, X11Forwarding, but I prefer to be explicit in my "just in case, no, you can't do that either".
  12. Send a SIGHUP to sshd to pick up the new configuration:
    kill -HUP $PIDOFSSHD
    
    or use your system's reload configuration utility like rcctl on OpenBSD to pick up the new configuration:
    rcctl reload sshd
    

And with that, you should have chrooted SFTP access for $NEWUSER

sftp user@hostname
(if not, check the tail of your /var/log/authlog for hints). However if you try to ssh, scp, or rsync, it should reject your efforts at the point you've entered your credentials:
ssh user@hostname
Password:
Permission denied, please try again.
⋮

echo test > delme
scp delme user@hostname:
Password:
Permission denied, please try again.
⋮

For additional customers, you can repeat steps 2 through 9. I've created a shell-script to do those steps as well as a bit of other administrivia for $DAYJOB but that should give you the basics.