An SFTP-only account on Ubuntu 24.04 lets a user transfer files over SSH without giving that account an interactive shell or unrestricted access to the server filesystem. The safest baseline is to use OpenSSH's built-in internal-sftp, place the user inside a root-owned chroot, give them a writable subdirectory inside that jail, and disable every SSH forwarding feature that the account does not need.
This tutorial uses a group-based OpenSSH configuration so you can add more restricted users later without duplicating per-user Match blocks. It also fixes several common mistakes in older SFTP guides: assuming a writable chroot root is allowed, editing only the main sshd_config, leaving a Match block open inside sshd_config.d, restarting the wrong daemon blindly, and checking syntax without verifying the effective configuration that actually applies to the restricted user.
Ubuntu's current OpenSSH documentation recommends modular files in /etc/ssh/sshd_config.d/, running sshd -t before a restart, and restarting ssh.service after normal server configuration changes. OpenSSH also requires every component of ChrootDirectory to be root-owned and not writable by other users or groups. internal-sftp is useful here because it runs inside sshd and does not require copying a shell, libraries, or device nodes into the jail.
Raff Technologies is the Ubuntu VM platform used by the original tutorial. The original setup was tested on Ubuntu 24.04 LTS on a Raff 2 vCPU / 4 GB RAM VM; the OpenSSH configuration and Ubuntu service guidance were re-verified on September 4, 2026.
Prerequisites:
- A Raff Linux VM running Ubuntu 24.04
- SSH access with a non-root sudo user
- A second SSH session available while you change
sshdsettings - Port
22/tcpor your existing SSH port allowed by the firewall - A test username that is not your current administrator account
The examples use the restricted account sftpclient and group sftponly.
Step 1 — Verify OpenSSH and keep your current admin session open
Check that the OpenSSH server package is installed:
dpkg -s openssh-server | grep -E '^(Status|Version):'
If it is not installed:
sudo apt update sudo apt install -y openssh-server
Check the service:
sudo systemctl is-active ssh.service
Check the server binary version:
/usr/sbin/sshd -V 2>&1 | head -1
Before changing anything, keep your current working SSH session open and, if possible, open a second administrator session. Do not test new SFTP rules by modifying the account you are currently using to administer the VM.
Ubuntu's current OpenSSH server documentation warns that a bad SSH configuration can lock you out and recommends validating the configuration before restarting the service.
Verify: openssh-server should be installed, ssh.service should be active, and you should still have a working administrator session that is not the SFTP test account.
Step 2 — Back up the SSH configuration before editing it
Create a timestamped backup of the main configuration and the current snippet directory:
sudo cp -a /etc/ssh/sshd_config \ "/etc/ssh/sshd_config.backup.$(date +%Y%m%d-%H%M%S)" sudo cp -a /etc/ssh/sshd_config.d \ "/etc/ssh/sshd_config.d.backup.$(date +%Y%m%d-%H%M%S)"
Ubuntu includes this line near the top of its default server configuration:
Include /etc/ssh/sshd_config.d/*.conf
That means drop-in files are the preferred place for a small custom policy such as an SFTP-only group. Ubuntu also documents that OpenSSH generally uses the first value it obtains for most directives, which makes snippet ordering and Match scope worth checking rather than assuming.
Verify:
ls -ld /etc/ssh/sshd_config.backup.* /etc/ssh/sshd_config.d.backup.* | tail
A backup of both the main file and snippet directory should exist before you continue.
Step 3 — Create the SFTP-only group and user
Create the group:
sudo groupadd --force sftponly
Create the restricted test user if it does not already exist:
sudo useradd -m -s /bin/bash -G sftponly sftpclient
Set a password for the initial test:
sudo passwd sftpclient
This tutorial does not rely on /usr/sbin/nologin to enforce SFTP-only behavior. The OpenSSH Match Group block will force internal-sftp, disable forwarding, and disable TTY allocation. This avoids mixing shell-policy behavior with SFTP subsystem testing and makes the actual SSH restriction explicit and testable.
If the account already exists, add it to the group without removing its other memberships:
sudo usermod -aG sftponly sftpclient
Verify:
id sftpclient getent passwd sftpclient
The output should show membership in sftponly. Do not continue if you accidentally added your current administrator account to this group.
Step 4 — Create a root-owned chroot with a writable upload directory
OpenSSH checks ChrootDirectory ownership unconditionally. Every component of the chroot path must be root-owned and must not be writable by another user or group.
Use the user's home directory as the jail root:
sudo chown root:root /home/sftpclient sudo chmod 755 /home/sftpclient
Create a writable directory inside the jail:
sudo mkdir -p /home/sftpclient/uploads sudo chown sftpclient:sftpclient /home/sftpclient/uploads sudo chmod 750 /home/sftpclient/uploads
The resulting model is:
/home/sftpclient root:root 755 <- chroot root /home/sftpclient/uploads sftpclient:* 750 <- writable area
Do not make /home/sftpclient user-writable and do not use chmod 777. OpenSSH's Ubuntu 24.04 man page states that all path components used for ChrootDirectory must be root-owned and not writable by any other user or group.
Verify:
namei -l /home/sftpclient stat -c '%U:%G %a %n' /home/sftpclient /home/sftpclient/uploads
The chroot root should be root:root and not group/other writable; the uploads directory should be writable by sftpclient.
Step 5 — Create an isolated OpenSSH Match Group policy
Create a dedicated snippet:
sudo tee /etc/ssh/sshd_config.d/60-sftp-only.conf > /dev/null <<'EOF' Match Group sftponly ChrootDirectory %h ForceCommand internal-sftp -d /uploads DisableForwarding yes PermitTTY no PermitUserRC no Match all EOF
The important directives are:
ChrootDirectory %h— makes the user's real home directory the jail root;ForceCommand internal-sftp -d /uploads— forces the in-process SFTP server and starts the user in the writable/uploadsdirectory inside the jail;DisableForwarding yes— disables TCP, agent, X11, and StreamLocal forwarding in one directive;PermitTTY no— prevents TTY allocation;PermitUserRC no— prevents execution of user SSH rc files;Match all— ends the restricted Match scope before parsing continues.
That final Match all is especially important in a drop-in file. Ubuntu loads /etc/ssh/sshd_config.d/*.conf from the main configuration, and a Match context continues until another Match directive or end of the full configuration stream. Ending the snippet with Match all prevents later global settings from accidentally remaining inside the sftponly Match context.
You do not need to replace Ubuntu's global Subsystem sftp ... line for this design. ForceCommand internal-sftp directly forces the in-process SFTP server for matching users.
Verify:
sudo cat /etc/ssh/sshd_config.d/60-sftp-only.conf
The file should contain exactly one Match Group sftponly block followed by Match all.
Step 6 — Validate both syntax and the effective Match configuration
First run OpenSSH's syntax test:
sudo sshd -t
No output means the syntax and host-key checks passed.
Then inspect the effective configuration for sftpclient. sshd -T prints the resulting configuration, while -C applies Match rules for a simulated connection:
sudo sshd -T \ -C user=sftpclient,host=localhost,addr=127.0.0.1,laddr=127.0.0.1,lport=22 \ | grep -E '^(chrootdirectory|forcecommand|disableforwarding|permittty|permituserrc) '
Expected values include:
chrootdirectory %h forcecommand internal-sftp -d /uploads disableforwarding yes permittty no permituserrc no
This check is stronger than syntax validation alone because it proves that the Match Group policy actually applies to the account you intend to restrict.
Verify: sshd -t should produce no error and sshd -T -C ... should show the SFTP-only settings above for sftpclient.
Step 7 — Restart OpenSSH safely and verify administrator access first
Ubuntu's current OpenSSH server documentation recommends applying normal sshd configuration changes with:
sudo systemctl restart ssh.service
Do that only after sshd -t succeeds:
sudo systemctl restart ssh.service
Immediately verify the service:
sudo systemctl is-active ssh.service
Before closing your original SSH window, open a new administrator SSH session and make sure it works.
Ubuntu 24.04 can use socket activation in some OpenSSH configurations, which matters particularly when changing listener settings such as Port or ListenAddress. This tutorial does not change those listener directives. For the Match policy used here, follow Ubuntu's documented ssh.service restart path and validate the actual result with a fresh connection.
Verify: ssh.service should be active and a new administrator SSH connection should succeed before you test sftpclient.
Step 8 — Test SFTP login, landing directory, upload, and download
From another machine or another local terminal, connect with SFTP:
sftp sftpclient@SERVER_IP
After authentication, confirm the working directory:
sftp> pwd
Because the Match policy uses internal-sftp -d /uploads, the user should start in:
Remote working directory: /uploads
Create a local test file before connecting, or use an existing harmless file. From the SFTP prompt:
sftp> put test-upload.txt sftp> ls -la sftp> get test-upload.txt test-download.txt
Confirm that the user cannot leave the jail:
sftp> cd / sftp> ls
The / visible to this user is the chroot root, not the real server filesystem root. Paths such as the server's real /etc are outside the jail.
Verify: The user should land in /uploads, be able to upload/download there, and remain confined to the chroot filesystem.
Step 9 — Verify shell commands, TTYs, and forwarding are blocked
SFTP success is only half the test. Confirm that the same account cannot obtain a shell command channel:
ssh sftpclient@SERVER_IP 'id'
Because ForceCommand internal-sftp replaces the requested command, this should not execute id as a shell command.
Try an interactive SSH session:
ssh sftpclient@SERVER_IP
The account should not receive a normal shell prompt.
The DisableForwarding yes rule also blocks the account from being used for SSH forwarding. For example, a local forwarding request should fail rather than create a tunnel:
ssh -N -L 15432:127.0.0.1:5432 sftpclient@SERVER_IP
Do not leave this command running if your client behaves unexpectedly; stop it with Ctrl+C.
OpenSSH documents that ForceCommand alone does not disable other forwarding channels, which is why this tutorial also sets DisableForwarding yes.
Verify: sftpclient should transfer files through SFTP but should not receive a shell, execute arbitrary SSH commands, allocate a TTY, or create SSH forwarding channels.
Step 10 — Restrict network access when the SFTP client has a stable source IP
SFTP uses SSH transport, so the network port is the server's SSH port — normally TCP 22 unless you deliberately changed it.
If this SFTP account is used only from a known office, VPN, CI runner, or partner IP, firewall restriction adds a useful independent control.
For example, with UFW and a stable client address 198.51.100.20:
sudo ufw allow from 198.51.100.20 to any port 22 proto tcp
Do not remove your broader administrator SSH rule until you have confirmed how all legitimate administrators connect. A firewall change can lock you out independently of the SFTP configuration.
If your VM also uses a cloud-side security group, apply the same source-IP principle there when appropriate.
For a complete firewall workflow, use Set Up UFW Firewall on Ubuntu 24.04.
Verify: Review the firewall with sudo ufw status numbered and confirm it still permits every required administrator path as well as the intended SFTP client source.
Step 11 — Monitor SFTP authentication and errors with journalctl
Ubuntu's current server documentation uses the ssh.service journal for OpenSSH troubleshooting. Follow logs while you test:
sudo journalctl -fu ssh.service
Or inspect recent entries:
sudo journalctl -u ssh.service -n 100 --no-pager
Useful problems to look for include:
bad ownership or modes for chroot directory— a path component is not root-owned or is writable by another user/group;- authentication failures — password/key or account-policy problem;
Connection closedimmediately after authentication — often a chroot, directory, or Match-policy problem;- missing
/uploads— the forced start directory does not exist inside the jail.
Do not depend on hard-coded /var/log/auth.log availability for every Ubuntu logging setup; the systemd journal is the most portable first check on current Ubuntu Server.
Verify: A successful SFTP connection should appear in the SSH journal without a chroot ownership error or repeated authentication failure.
Step 12 — Add another SFTP-only user without editing sshd_config again
Create another user and add it to the same group:
sudo useradd -m -s /bin/bash -G sftponly client2 sudo passwd client2
Make the new home directory a valid chroot root:
sudo chown root:root /home/client2 sudo chmod 755 /home/client2
Create the writable directory:
sudo mkdir -p /home/client2/uploads sudo chown client2:client2 /home/client2/uploads sudo chmod 750 /home/client2/uploads
No OpenSSH configuration edit is necessary because the group Match rule already exists. No service restart is required merely because a new user joined the group.
Check that the effective Match configuration applies to the second user as well:
sudo sshd -T \ -C user=client2,host=localhost,addr=127.0.0.1,laddr=127.0.0.1,lport=22 \ | grep -E '^(chrootdirectory|forcecommand|disableforwarding) '
Verify: client2 should inherit the same chroot, forced SFTP command, and forwarding restrictions without another sshd_config change.
Step 13 — Run the final end-to-end SFTP security check
Run the final checklist before using the account for real file transfers.
On the server:
sudo sshd -t sudo systemctl is-active ssh.service stat -c '%U:%G %a %n' /home/sftpclient /home/sftpclient/uploads sudo sshd -T \ -C user=sftpclient,host=localhost,addr=127.0.0.1,laddr=127.0.0.1,lport=22 \ | grep -E '^(chrootdirectory|forcecommand|disableforwarding|permittty|permituserrc) '
From the client:
sftp sftpclient@SERVER_IP
Inside SFTP:
pwd put test-upload.txt ls -la
In another client terminal:
ssh sftpclient@SERVER_IP 'uname -a'
The SFTP operation should work, while the arbitrary SSH command should not run as a shell command.
