This project aims to provide a working solution for home directory-jailed SFTP using OpenSSH-server's internal-sftp subsystem and automount(8).
Motivation
FTP daemons like ProFTPd can restrict users to their home directories (and do so by default). This is often desirable for shared systems.
SSH daemons like the OpenSSH Server lack that feature.
While it is easy to only enable the SFTP facilities for certain users
(by adding ForceCommand internal-sftp
to the sshd_config file, for example),
those users will still be able to traverse the entire file system,
making it necessary for other users to pay close attention to their homedir modes.
Another problem is that the system might have sub accounts with a User Private Groups scheme, commonly seen at shared web hosting providers. For example:
- Main account
uid=1000(mle) gid=1000(mle) groups=(1000)mle,(500)sftp
with homedir/home/mle
should be able to access /home/mle via SFTP, including all sub-directories. - Sub account
uid=1001(mle-sub) gid=1000(mle) groups=(1000)mle,(500)sftp
with homedir/home/mle/sub
should be able to access /home/mle/sub via SFTP, but not the rest of /home/mle.
If those two accounts both have a umask of 002
,
then the main account can access the sub account's files and directories.
The sub account however should be prevented from leaving their own homedir
while using SFTP, just like with FTP.
Existing solutions often advise administrators to have root-owned home directories only and have the SSH daemon use the chroot(2) system call to lock users into their home directories. While effective, this approach has distinct disadvantages:
- Users can no longer change their home directory mode.
- Users will see their own home directory as
/
.
(This is also the case with most FTP daemons.)
This project presents a solution which avoids those problems.
Goals
Have an
sftp
user group which allows SFTP logins to its members but prevents them from using regular SSH.Force SFTP users to be locked into their home directories to isolate them from all other homes and system directories.
Support sub-accounts with nested home directories (see above for an example).
Minimal configuration – the installation should set up everything by itself.
No administrative overhead – there should be no need, say, to change the way users are added to the system.
Have consistent path names – the
pwd
SFTP command should still show the real homedir path.
Ownership problem
While the internal-sftp
sshd subsystem allows "lightweight" chrooting,
that is, without requiring /dev/null, /bin/sh, etc,
it still comes with a serious restriction:
The chroot dir has to be owned by root
.
We don't want to force our users to have root-owned homedirs.
Therefore, an sshd_config directive like ChrootDirectory %h
won't work.
(Also, the resulting directory hierarchy would look weird to the SFTP users:
their homedir would appear to be located at /
.)
We'll have to build an additional hierarchy for every user:
A root-owned directory like /jail/USERNAME
can be used as one user's chroot filesystem.
In there, they should find their homedir (and nothing more).
Symlinks won't work for that
(they'd be evaluated inside the chroot filesystem, leading nowhere),
and not even root is allowed to create directory hardlinks.
Bind mounts to the rescue
Bind mounts are a way to have one directory in two places, sort of like temporary directory hardlinks.
For example, this bind mount makes my home directory available at a second location:
$ ls -ld /mnt
drwxr-xr-x 2 root root 4,0K Jul 19 2015 /mnt/
# mount --bind $HOME /mnt
$ ls -ld /mnt
drwxr-x---+ 77 mle mle 4,0K Jun 2 00:20 /mnt/
As can be seen, the secondary location now has the same ownership and mode as my "real" homedir. Those two directories are now equal in every way. It will last until /mnt gets unmounted or the machine reboots.
Security problems with bind mounts
They also pose a security risk if applied too carelessly.
Usually, setting one's homedir mode to 0700
is sufficient to protect all of its contents.
Even subdirectories and files with permissive modes like 0777
are still totally safe from other users
simply because nobody can enter such a homedir at all.
Bind mounts circumvent this, just like hardlinks do!
For example:
# ls -la /root
drwx------ 25 root root 4,0K Jun 2 01:09 /root/ # No one can get in here.
drwxrwxrwx 2 root root 4,0K Jun 2 01:09 /root/secrets/ # Perfectly safe, right?
# mount --bind /root/secrets /mnt
$ ls -ld /mnt
drwxrwxrwx 2 root root 4,0K Jun 2 01:09 /mnt/ # Oops!
Because /mnt
can be reached by everyone,
everybody can now effectively enter /root/secrets/
.
It is no longer protected by /root/
's restrictive mode.
It is therefore root's responsibility to be very careful with bind mounts in general, because they can be used to circumvent the mode restrictions of nested directories.
Using bind mounts for ssh chrooting
Keeping this problem in mind,
we can create bind mounts to homedirs
in root-owned locations.
This evades sshd's restriction that the chroot dir must be root-owned:
We can duplicate the /home
hierarchy for one user inside their personal chroot filesystem.
We start by creating a base directory /jail
for our bind mount points
which is owned by root and has very restrictive modes,
so no one can enter it.
(This will still work because internal-sftp runs as root
prior to chrooting,
therefore it can enter the /jail directory,
whereas logged-in non-root users cannot.)
# mkdir -m 0700 /jail
Now assume we want to allow chrooted SFTP logins for the mle-sub
user.
Their homedir is /home/mle/sub
.
# mkdir -p /jail/mle-sub/home/mle/sub # all owned by root
# mount --bind /home/mle/sub /jail/mle-sub/home/mle/sub
The chroot dir /jail/mle-sub
is owned by root,
so the ChrootDirectory
directive will accept it.
Inside it, there's a duplicate of the user's homedir and nothing else.
That homedir duplicate is linked with the real homedir,
so it really has the same permissions, ownership, and of course content.
Getting sshd on board
Appending this block to /etc/ssh/sshd_config
tells sshd that all members of the sftp
group
are to be chrooted and can only ever use SFTP:
Match group sftp
ChrootDirectory /jail/%u # sshd will replace %u with the username.
ForceCommand internal-sftp
So far so good
Now when the user mle-sub
establishes a SFTP connection,
sshd will first chroot the session to /jail/mle-sub
,
which works because those directories both belong to root.
As usual, internal-sftp will then try to change into the user's homedir,
which is /home/mle/sub
.
That directory exists inside the chroot jail as well, so the directory change succeeds.
home
and home/mle
are only empty, non-writable directories inside the jail,
so the user cannot do any harm there,
while home/mle/sub
is a bind link to the real homedir.
The user ends up with an SFTP session inside their own home directory –
or at least that's what it'll look like.
Escaping that homedir with cd ..
will only land them in an empty, non-writable directory,
not in the real /home/mle
.
Success!
Too many mounts, too much work
To enable SFTP logins like that for all sftp
group members,
we'd have to do this for every one of them:
- Replicate their homedir structure inside /jail
(# mkdir -p /jail/$username/$homedir
) - Bind mount their homedir to the fake homedir inside the jail
(# mount --bind $homedir /jail/$username/$homedir
)
And of course, we'd have to do this after every reboot, because bind mounts are not durable.
We could enter all of them into our /etc/fstab
,
but after a few dozen entries
both that file and mtab
would get quite cluttered.
Apart from that, it would necessitate changes to /etc/fstab
every time a sftp
user gets added, deleted, or has their homedir changed.
What if the system could automatically create and mount those directories when needed, unmounting and removing them again after the SFTP session ended?
Enter autofs
The autofs package contains the automount(8) daemon and the autofs4 kernel module. It is commonly used to establish NFS network mounts when they are needed, automatically unmounting them again if not accessed for some time.
It can read its directory configuration both from static map files, amounting to little more than fstab with automatic network retry and timeouts. It can also ask a script for its per-directory configuration every time a filesystem access to a missing directory is made. (Dynamic mappings like this can be used, for example, to have a zero-configuration "network shares" directory which will automatically connect to smb servers.)
We'll use this dynamic mapping scheme to automatically mount our jailed home directory links when they are needed.
Top configuration file
The top configuration files go in /etc/auto.master.d/
(which might have to be created first).
We'll add our own configuration there:
$ cat > /etc/auto.master.d/jails.autofs
/jail program:/etc/autofs-sftp-jails.sh --timeout=20
This oneliner basically says that
whenever someone tries to enter a non-existing directory in /jail
,
automount should call our autofs-sftp-jails.sh script
which should then determine where to mount that directory from.
The dynamic map script
The autofs-sftp-jails.sh receives the non-existing relative directory
as its first argument.
Since only internal-sftp will ever enter /jail
and we configured it to chroot the users to /jail/%u
,
we know that the autofs-sftp-jails.sh script
will always receive the username of the SFTP user as its first argument.
With that, the script performs several security checks first:
- Making sure that
/jail
belongs to root and has a restrictive mode (0700
), so that no one can go there to circumvent homedir access restrictions. - Making sure that the
$1
username actually exists and has an existing homedir. - Making sure that the user's homedir is actually a directory, not a symlink, or autofs will happily create a bind link to the link target (!).
If everything looks good, the script echoes a single configuration line for the automount daemon like this:
echo "-fstype=bind \"/$homedir\" \":$homedir\""
Let's break that down:
-
-fstype=bind
– pretty self-explanatory. (automount's default filesystem is NFS.) -
"/$homedir"
– this tells automount which mount point to create/use. It is relative to the non-existing directory which triggered the script, so it'll be expanded to/jail/$username/$homedir
. -
":$homedir"
– this tells automount where to find the mount target.
Effect
This means that
as soon as the mle-sub
user logs in via SFTP,
sshd will chroot them to /jail/mle-sub
.
This directory does not yet exist, triggering the automount daemon,
which will then create a bind mount
at /jail/mle-sub/home/mle/sub
leading to the real /home/mle/sub
.
(The intermediary path components are automagically created, belonging to root with a mode of 0755
, so thankfully our script won't have to do that.)
The SFTP user will end up in /jail/mle-sub/home/mle/sub
which to them looks and feels like the real /home/mle/sub
yet is completely isolated from the rest of the /home/mle
tree!