Last modified 6 years ago Last modified on 22.04.2011 21:37:05


Version 2.0 is out

This is a complete rewrite of sftponly. It enabled a way to support scp and rsync protocols, not only sftp, and makes building a package much easier. Version 2.0 also changes location of binary: instead of /usr/sbin/sftponly now you get /usr/bin/sftponly.


Remember scponly, a shell providing copy-only access to users?

Want to close them in chroot() environment?

Don't want to bother with creating full-blown chroot() or even small /dev contents?

This is the (my) way to go.

sftponly doesn't need any files -- especially device files -- in user's home directory, so it's quite easy to manage multiple copy-only accounts. Your users will not be able to break their environment anymore.

How to download

git clone

How to install

NOTE: Installation process is tested for building packages. If you want to make mess in your system by omitting package system, you are on your own.

You need casual package building tools for your distribution, either rpm-build (RPMs) or dpkg-dev with fakeroot (DEBs).

Red Hat

  1. Prepare source RPM (non-root privileges are fine here)
    make srpm
  2. Build binary RPM (unless you've taken care of building as non-root, you need to be root here)
    rpmbuild --rebuild sftponly-*.src.rpm
  3. Install package (exact path should be printed by previous step); most probably it will be located in /usr/src/redhat/RPMS/noarch/sftponly-*.rpm


  1. Build binary package
    dpkg-buildpackage -b -uc
  2. Install package (../sftponly*.deb)

How to use

After installing a package you just need to set shell for selected user to /usr/bin/sftponly. The only thing sftponly needs is the user's home directory.

When user tries to login, sftponly chroots to his home and executes appropriate command: either /usr/bin/scp, /usr/bin/rsync or .../sftp-server (location varies, depending on distribution; usually it's somewhere in /usr/lib or /usr/libexec).


The only drawback of using sftponly is that paths specified in command line can't contain spaces (though subdirectories can) and wildcards are ignored. Example:

rsync -avz ssh://user@host:'/some where/subdir' .

This will be treated as two separate directories to download: /some and where/subdir.

Just avoid spaces in names. They're very troublesome anyway.

Technical details: how does it work?

Version 2.0

sftponly comes in two parts: SUID binary /usr/bin/sftponly and shared library

Binary sets $LD_PRELOAD environment variable to and executes desired program, either scp, rsync or sftp-server, effectively injecting libchroot's code into external program. contains thing called constructor: a function that is called before main(). Thanks to this, program can get chrooted even if it doesn't do so on its own, and yet still, the program binary doesn't need to lay under chroot directory.

There are some quirks that were necessary to address, though.


Some programs try to open /dev/null file. I wanted to make chroot accounts as easy in maintenance as possible, so a device file in chroot is a Bad Thing(tm), as it needs to be created by system administrator and in some cases could be removed by user. sftponly opens /dev/null before chrooting and intercepts open()/open64() calls, returning dup()ed file descriptor for each access try to /dev/null. The same trick is used for /dev/zero.

$LD_PRELOAD, setuid(0)

For SUID binaries $LD_PRELOAD environment variable is ignored (it's an approximation, but good enoguh). Linux ELF loader (/lib/ detects whether the binary is SUID-ed or not by comparing real UID with effective UID.

So, when you execute something from process that was created using SUID-ed binary, the spawned process will be treated as its binary was SUID-ed, too -- unless you have called setuid() or similar function.

Because of this, /usr/bin/sftponly calls setuid(0) and passes in an environment variable login that will be used in to setuid() back. Of course, login is checked with /etc/passwd to match RUID in the first place.

Version 1.0 (historic)

Version 1.0 was based on observation that once a shared library is loaded, it remains open and operative after chroot() call. Thus, sftp-server from OpenSSH was compiled in a tricky way and became a shared library. sftponly binary only needed to load this library, get address of main() function, go chroot() and call recently loaded main().

Because of compiling sftp-server as a shared library, build procedure required OpenSSH sources and was a bit troublesome. Making scp or rsync working this way was too difficult to maintain in my opinion, hence rewrite.