SFTP-only accounts are useful when a partner, automation job, or internal team needs to exchange files but should not receive a shell on the SSH server. The restriction has to block interactive SSH sessions while still allowing the SFTP subsystem to read and write the intended upload directory.
OpenSSH handles this with a Match block in /etc/ssh/sshd_config. A matched group can be forced into internal-sftp, placed in a per-user chroot under /srv/sftp, and denied forwarding features that would otherwise keep extra SSH channels available.
The chroot boundary is strict about ownership. The directory named by ChrootDirectory and every parent in that path must be owned by root and not writable by group or others, while a separate child directory inside the jail can be owned by the SFTP user for uploads. Keep an existing admin session open while changing sshd so a bad edit can be rolled back from the current connection or a console.
Related: How to copy a file over SSH
Related: How to configure SSH Match blocks
Related: How to disable SSH TCP forwarding
$ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
Do not close your current admin session until the restricted account has been tested. A syntax error or wrong Match rule can block new SSH logins.
$ sudo groupadd --system sftpusers
Using a separate group keeps the SFTP-only policy separate from normal SSH users.
$ sudo useradd --no-create-home --home-dir / --shell /usr/sbin/nologin --gid sftpusers sftpuser1
The home directory is set to / because the account will be chrooted to /srv/sftp/sftpuser1 before internal-sftp starts.
$ sudo passwd sftpuser1 New password: Retype new password: passwd: password updated successfully
If the server uses key-only authentication, install the user's public key through the same approved process used for other SSH accounts before testing SFTP.
$ sudo mkdir -p /srv/sftp/sftpuser1/upload
$ sudo chown root:root /srv/sftp /srv/sftp/sftpuser1
$ sudo chmod 755 /srv/sftp /srv/sftp/sftpuser1
sshd refuses the login if /srv/sftp or /srv/sftp/sftpuser1 is writable by sftpuser1 or by sftpusers. Put writable content below the chroot directory, not on the chroot directory itself.
Tool: chmod Calculator
$ sudo chown sftpuser1:sftpusers /srv/sftp/sftpuser1/upload
$ sudo chmod 750 /srv/sftp/sftpuser1/upload
$ sudo ls -ld /srv/sftp /srv/sftp/sftpuser1 /srv/sftp/sftpuser1/upload drwxr-xr-x 3 root root 4096 Jun 13 11:48 /srv/sftp drwxr-xr-x 3 root root 4096 Jun 13 11:48 /srv/sftp/sftpuser1 drwxr-x--- 2 sftpuser1 sftpusers 4096 Jun 13 11:48 /srv/sftp/sftpuser1/upload
$ sudoedit /etc/ssh/sshd_config
Subsystem sftp internal-sftp
Keep only one active Subsystem sftp line. Comment out an older line that points to an external sftp-server binary.
Match Group sftpusers
ChrootDirectory /srv/sftp/%u
ForceCommand internal-sftp
DisableForwarding yes
PermitTTY no
The %u token expands to the login name, so sftpuser1 is jailed under /srv/sftp/sftpuser1. DisableForwarding blocks forwarding channels, and PermitTTY no prevents terminal allocation for matched users.
Related: How to configure SSH Match blocks
Related: How to disable SSH TCP forwarding
$ sudo sshd -t
No output means the configuration parsed successfully.
Related: How to test SSH server configuration
$ sudo systemctl restart ssh
On RHEL, CentOS, Fedora, and similar systems, the service name is commonly sshd instead of ssh.
$ sftp sftpuser1@host.example.net sftp> pwd Remote working directory: / sftp> ls upload sftp> cd upload sftp> put local-file.txt local-file.txt Uploading local-file.txt to /upload/local-file.txt sftp> ls local-file.txt sftp> rm local-file.txt sftp> bye
The restricted account starts at the chroot root and can write only inside /upload.
$ ssh -T sftpuser1@host.example.net This service allows sftp connections only.
The nonzero SSH exit and internal-sftp message confirm that the matched account is forced into SFTP instead of a shell or remote command session.