How to Chroot SFTP-only Users into Their Home Directory in Linux

When managing a Linux server, you often need to grant file transfer access to clients, partners, or external collaborators. From a security standpoint, two fundamental requirements usually arise:

  1. SFTP-only access: Prevent interactive SSH shell access to the command line.
  2. Chroot jail: Confine users strictly to their own home directory, preventing them from exploring system directories (e.g., /etc, /var) or other users’ files.

Historically, this often required setting up standalone FTP servers (such as vsftpd or pure-ftpd) or creating complex chroot environments filled with cloned /bin binaries and shared libraries (.so).

Fortunately, modern OpenSSH with built-in internal-sftp and ChrootDirectory achieves this natively, securely, and with zero external dependencies.


Step 1: Configure OpenSSH Daemon

Open the SSH configuration file:

sudo vim /etc/ssh/sshd_config

Tip: On modern Linux distributions, you can also create a standalone file under /etc/ssh/sshd_config.d/sftp.conf to avoid merge conflicts during future package updates.

1. Ensure internal-sftp is Enabled

Locate the Subsystem sftp directive and ensure it uses internal-sftp:

Subsystem sftp internal-sftp

Unlike external binaries like /usr/lib/openssh/sftp-server, internal-sftp runs inside the sshd process, requiring no additional binaries or libraries inside the chroot jail.

2. Append the Match Group Rules

Place the following block at the very end of sshd_config (directives after a Match block apply only to that criteria until the next Match or the end of the file):

# Rules for the sftponly group (must be at the bottom of the config)
Match Group sftponly
	ChrootDirectory %h
	ForceCommand internal-sftp
	AllowTcpForwarding no
	X11Forwarding no
	AllowAgentForwarding no
  • Match Group sftponly: Applies these settings to any user in the sftponly group.
  • ChrootDirectory %h: Locks the session root to the user’s home directory (%h).
  • ForceCommand internal-sftp: Forces the session to run only the SFTP subsystem, disallowing arbitrary shell execution.
  • AllowTcpForwarding no / X11Forwarding no / AllowAgentForwarding no: Closes tunneling vectors, preventing unauthorized proxying or port forwarding.

Step 2: Create Group and User Accounts

1. Create the Dedicated Group

sudo groupadd sftponly

2. Create the User Account

Create a new user assigned to the sftponly group with shell access disabled:

sudo useradd -g sftponly -d /home/{username} -s /usr/sbin/nologin -m {username}
sudo passwd {username}

For an existing user, update their profile with:

sudo usermod -g sftponly {username}
sudo usermod -s /usr/sbin/nologin {username}
sudo usermod -d /home/{username} {username}

(Note: On older systems, /bin/false can be used instead of /usr/sbin/nologin.)


Step 3: Configure Permissions (The Critical Chroot Rule)

This step causes the vast majority of chroot setup failures.

OpenSSH enforces strict security requirements:

The directory specified by ChrootDirectory (and every component in its parent path) must be owned by root and must not be writable by any other user or group (maximum permission of 755, never 775 or 777).

If ownership or permissions are incorrect, the connection will drop immediately with an error like Write failed: Broken pipe or fatal: bad ownership or modes for chroot directory.

Fix the permissions with:

# Both /home and /home/{username} must be owned by root with 755 permissions
sudo chown root:root /home/{username}
sudo chmod 755 /home/{username}

Step 4: Create a Writable Upload Directory

Because /home/{username} is owned by root with 755 permissions, the user has read-only access to their root directory and cannot upload files directly there.

The standard solution is to create a dedicated writable subfolder:

# Create the uploads subfolder
sudo mkdir -p /home/{username}/uploads

# Grant ownership to the SFTP user
sudo chown {username}:sftponly /home/{username}/uploads
sudo chmod 755 /home/{username}/uploads

Once connected, the user will be jailed inside /home/{username}, while being fully able to upload, modify, and delete files inside uploads/.


Step 5: Test and Restart the SSH Service

Always test the configuration syntax before restarting:

sudo sshd -t

If there is no output, the configuration syntax is valid.

Restart the SSH service using systemd:

  • Ubuntu / Debian:
    sudo systemctl restart ssh
  • RHEL / CentOS / Alma / Rocky Linux:
    sudo systemctl restart sshd

Step 6: Verify Connections

1. Test SFTP File Transfer

From a client machine:

sftp {username}@{server_ip}

Once logged in, pwd will show / (which maps securely to /home/{username} on the host). Navigate into uploads to transfer files:

sftp> cd uploads
sftp> put test.txt
Uploading test.txt to /uploads/test.txt
sftp> ls
test.txt

2. Verify Shell Access is Blocked

Attempt an interactive SSH terminal connection:

ssh {username}@{server_ip}

The server should refuse the shell prompt and disconnect immediately:

This service allows sftp connections only.
Connection to server_ip closed.

This confirms that shell access is successfully denied and the jail is securely enforced.


Troubleshooting

  • Error: Write failed: Broken pipe or Connection reset by peer
    • Cause: Almost always an ownership or permission violation on the chroot path.
    • Remedy: Verify that /home and /home/{username} are owned by root:root and set to 755:
      ls -ld /home /home/{username}
      # Expected: drwxr-xr-x root root ...
  • Inspect Live Auth Logs:
    # Debian / Ubuntu
    sudo journalctl -u ssh -e
    # or auth log
    sudo tail -f /var/log/auth.log