An SFTP-only account lets a designer, a client or a backup tool upload files without getting a shell on your server. With a chroot, the user also cannot see anything outside one folder. OpenSSH can do all of this on its own with a few lines of config. The tricky part is folder ownership, which this guide explains step by step.
Before you start
- A Linux VPS with Ubuntu 22.04 or 24.04, Debian 12, or AlmaLinux or Rocky Linux 8 or 9.
- Root or sudo access over SSH, and a second SSH session open while you change the SSH config.
- An SFTP client to test with, such as WinSCP or FileZilla.
Warning: A mistake in sshd_config can stop new SSH logins, including yours. Always run sudo sshd -t before reloading, and keep an existing session open until a fresh login works. The browser console in your VPS provider's control panel is the way back in if you get locked out.
How a chroot works
ChrootDirectory makes a folder appear as / to the user. They cannot go above it. For security, OpenSSH enforces strict rules:
- The chroot folder and every folder above it must be owned by
root. - None of them may be writable by any other user or group.
So the user cannot write to the top of their own chroot. You give them a writable subfolder inside it instead.
Create a group and a user
Using a group means one Match block covers every SFTP-only account:
sudo groupadd sftponly
sudo useradd -g sftponly -s /usr/sbin/nologin -d /srv/sftp/client1 -M client1
sudo passwd client1-s /usr/sbin/nologinblocks shell logins.-dsets the home folder to the future chroot path.-Mskips creating the home folder; you will build it by hand with the right ownership.
Build the folder structure
sudo mkdir -p /srv/sftp/client1/uploads
sudo chown root:root /srv/sftp /srv/sftp/client1
sudo chmod 755 /srv/sftp /srv/sftp/client1
sudo chown client1:sftponly /srv/sftp/client1/uploads
sudo chmod 750 /srv/sftp/client1/uploadsThe result:
drwxr-xr-x root root /srv/sftp
drwxr-xr-x root root /srv/sftp/client1
drwxr-x--- client1 sftponly /srv/sftp/client1/uploadsThe user lands in a read-only top folder and works inside uploads.
Configure OpenSSH
Ubuntu, Debian, AlmaLinux 9 and Rocky 9 read extra files from /etc/ssh/sshd_config.d/. Check that the main file contains the include line:
grep -i '^Include' /etc/ssh/sshd_configIf it prints Include /etc/ssh/sshd_config.d/*.conf, create a drop-in:
sudo nano /etc/ssh/sshd_config.d/50-sftponly.confOn a system without the include line, such as AlmaLinux or Rocky 8, add the same block at the very end of /etc/ssh/sshd_config instead.
Match Group sftponly
ChrootDirectory %h
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
PermitTunnel noMatch Group sftponlyapplies the lines below only to members of that group.ChrootDirectory %huses each user's home folder as their chroot.ForceCommand internal-sftpruns the built-in SFTP server, which needs no extra files inside the chroot.- The other lines block tunnelling tricks.
The main config should also use the built-in SFTP server. Check the Subsystem line:
grep -i '^Subsystem' /etc/ssh/sshd_configSubsystem sftp /usr/lib/openssh/sftp-server (Debian, Ubuntu) or /usr/libexec/openssh/sftp-server (AlmaLinux, Rocky) is fine, because ForceCommand internal-sftp overrides it for this group.
Test and reload:
sudo sshd -t
sudo systemctl reload sshOn AlmaLinux and Rocky the service is called sshd, so use sudo systemctl reload sshd.
Note: If password logins are disabled with PasswordAuthentication no, the SFTP user needs an SSH key, or you can add PasswordAuthentication yes inside the Match block for this group only.
Set a sensible upload umask (optional)
To control permissions on uploaded files, add a umask to the forced command:
ForceCommand internal-sftp -u 0027New files will then be 640 and new folders 750.
Test with WinSCP
- Open WinSCP and choose New Site.
- Set File protocol to SFTP, Host name to
203.0.113.10, Port to22(or your custom SSH port), and enterclient1and its password. - Click Login. You should see
/with a single folder,uploads. - Upload a file into
uploads. Try to create a folder at the top level; it should fail with a permission error.
Check it works
Confirm that a shell login is refused:
ssh client1@203.0.113.10This service allows sftp connections only.
Connection to 203.0.113.10 closed.Then connect with sftp client1@203.0.113.10 and run pwd. It should print /, and cd.. should keep you there.
Troubleshooting
- Login fails at once with "broken pipe" or "connection closed": chroot ownership is wrong. Read the log with
sudo journalctl -u ssh -n 20(or-u sshd) and look forbad ownership or modes for chroot directory. Fix it withchown root:rootandchmod 755on every folder in the path. - Login works but uploads fail: the user is writing to the top folder. Upload into
uploads, or check its ownership. - Match block is ignored: another
Matchblock or a later setting overrides it. Place it at the end of the file, and confirm withsudo sshd -T -C user=client1 | grep -i chroot. - Permission denied with the right password: password logins are turned off for this user. Allow them in the
Matchblock or use a key. See Fix WinSCP and FileZilla permission denied.
Next steps
- Point the chroot at a website folder: SFTP-only user for a website.
- Use keys in your client: WinSCP login with an SSH key.
- Learn the transfer basics: Transfer files with SCP and SFTP.
