Restricted SFTP access in Debian

As I’ll elaborate on in a few days1, when I added rate-limiting to nginx, I unintentionally blocked some legitimate traffic. Rather than make exceptions for these sources, I chose to provide certain services with read-only SFTP access to the specific directories they require.

It’s worth noting that in my case, I needed to grant particular users, not user groups, access to certain directories. Also, I have no need for any of these special users to access the same items. As a result, the following is tailored to user-level access to discrete directories, but can be set up using groups instead. I won’t detail that here, but the following should be sufficient for one to extrapolate how it would work for groups and shared directories.

Configuring this access broadly consists of three steps:

  • create the user and its home directory, which will hold the read-only mirror;
  • configure sshd to permit only SFTP connections, and limit access to a particular directory; and
  • configure the filesystem to mount read-only mirrors within the new user’s home directory.

It’s also worth noting that, while my examples reference WordPress and the VaultPress plugin, that’s simply a demonstration of use case. Nothing that I describe herein is particular to any service or CMS; the following requires only Debian and OpenSSH.

Adding a user

Since the directories I need to provide access to are specific to each read-only user I’ll create, I’m able to use the user’s home directory as its jail. If multiple users were to access the same directory, this approach wouldn’t be appropriate, but for my needs, it makes the configuration very logical.

First, because this user should have access the server via SFTP, an appropriate shell environment should be used to ensure that the user can’t gain unwanted SSH access. Fortunately, OpenSSH provides an SFTP-only shell that we can assign to the user, after one small change. For the OpenSSH server to be accepted as a login shell, it must be listed in /etc/shells. If /usr/lib/openssh/sftp-server isn’t listed in your /etc/shells, add it with this command:

echo '/usr/lib/openssh/sftp-server' >> /etc/shells

Now we can create the user:

useradd -s /usr/lib/openssh/sftp-server -d /home/vp vp

The above command creates the user vp with the home directory /home/vp and the login shell we just configured. The -d parameter is redundant because it specifies the default value, but I’ve included it for clarity.

If you’re confused right now as to why the user’s home directory isn’t set to that which holds the files the user will have access to, don’t worry–we’ll get to that part in due time. As I mentioned, we’ll share a read-only mirror with the new user (vp in my example) in the third part of this discussion.

Configuring sshd

With the user and home directory created, we can configure sshd to restrict that user’s access to an SFTP connection to its home directory. We do so by leveraging OpenSSH’s chroot support.

Using chroot restricts a user and its operations to the directory specified. As the name implies, it modifies which directory appears to be the root directory for the user’s processes. This “jail” doesn’t allow the user to explore anywhere beyond the specified directory. ls -la / will display the contents of the chroot’d directory, not the server’s actual root directory.

First, if for some reason /home/vp wasn’t created by the useradd command, create it:

mkdir /home/vp

Next, set the directory’s permissions as required for chroot‘ing:

chown -R root:root /home/vp

Notice that the directory is owned by root, not the vp user. A proper chroot environment uses a directory within a root-owned directory; in our case, /home/vp is the root-owned directory. This approach will also ensure that the user won’t have SFTP access to sensitive directories, such as /home/vp/.ssh/. To that end, create the directory that will serve as the chroot:

mkdir /home/vp/chroot
chown root:root /home/vp/chroot

While we’re making directories, let’s add the user’s SSH configuration directory:

mkdir /home/vp/.ssh
chown vp:vp /home/vp/.ssh

As we’ll see, while the .ssh directory is owned by the vp user, that user won’t have access to it via SFTP.

With the requisite directories in place, we can configure sshd and its chroot jail. To the end of your /etc/ssh/sshd_config, add:

# User and group restrictions
Match User vp
	ChrootDirectory /home/vp/chroot
	AllowTCPForwarding no
	X11Forwarding no
	ForceCommand internal-sftp

Depending on the age of your sshd configuration, you may need to change the SFTP subsystem to:

Subsystem sftp internal-sftp

Older configurations may specify the login shell I referenced earlier, /usr/lib/openssh/sftp-server; replacing this with internal-sftp had no negative impact on my VPS, and is needed for the preceding configuration change.

At this point, restart sshd.

To test that the configuration is working as expected, I’ll rely on public-key authentication2. To begin, we need an authorized_keys file owned by the vp user:

sudo -u vp -H touch /home/vp/.ssh/authorized_keys

I temporarily added my public key to this file to confirm that I could connect via SFTP, and that SSH connections were denied. When attempting to connect via SSH, you should receive this message:

>> ssh
This service allows sftp connections only.
Connection to closed.

If you’re able to connect and perform functions not supported by SFTP, confirm that you reloaded sshd and that the configuration changes were saved.

At this point, the vp user has restricted access to an empty directory, which is owned by the root user. It’s time to mirror some directories.

Adding read-only mounts

Since we’re using a chroot jail, symbolic links aren’t an option. While I can create them as the root user, the vp user won’t be able to follow them if they lead outside of the chroot directory. In their place, we’ll use mount binds, and add them to /etc/fstab to guarantee persistence. Besides being something supported within a chroot jail, the mount command supports a read-only flag (-o ro), to further enforce that aspect of this setup.

Using mount involves two steps: creating placeholders for the mirrored directories, and specifying the mounts.

First, within /home/vp/chroot, I created one file and one directory. The file is necessary because my wp-config.php is one directory above my WordPress webroot, as the CMS supports.

touch /home/vp/chroot/wp-config.php
mkdir /home/vp/chroot/public_html
chown vp:vp /home/vp/chroot/*

In the above, replace public_html with whatever you’ve named your webroot. If your wp-config.php is in the same directory as wp-settings.php, omit the touch step, and any further commands related to wp-config.php. Also, be sure not to change the ownership of /home/chroot/public_html itself, as that should remain owned by the root user and group; the vp user should only own the contents of /home/chroot/public_html, not the directory itself.

With the placeholders created, we can link them to their sources:

mount --bind -o ro /srv/www/wordpress/wp-config.php /home/vp/chroot/wp-config.php
mount --bind -o ro /srv/www/wordpress/public_html /home/vp/chroot/public_html

Assuming no errors with the above commands, the placeholders should reflect the same contents as their sources. For example, ls -la /home/vp/chroot/public_html will match the results of ls -la /srv/www/wordpress/public_html.

So that these changes persist server restarts, they must be defined in /etc/fstab:

/srv/www/wordpress/wp-config.php   /home/vp/chroot/wp-config.php   none   bind,ro   0 0
/srv/www/wordpress/public_html/   /home/vp/chroot/public_html/   none   bind,ro   0 0


At this point, despite restarts, the vp user will have read-only access to my WordPress installation’s webroot and wp-config.php.

From the VaultPress dashboard, configuring SFTP access generates a public key, which is added to the /home/vp/.ssh/authorized_keys file created earlier. VaultPress will test the connection and use it automatically.

  1. That post started as an introduction to this one, then approached 500 words, which called for excision.
  2. I don’t support password authentication on my VPS to begin with, and VaultPress–the motivator behind this–supports public-key auth