SFTP is the simplest secure way to move files to and from a Linux server. It runs entirely over SSH, so everything, your credentials and your files, is encrypted in transit and you only ever need one port open: 22. The catch is that out of the box, any user with SSH access can browse your whole filesystem. This guide fixes that properly: you will create a dedicated SFTP-only user, lock them inside a chroot jail so they can never leave their folder, and switch them to key-only login. About ten minutes on Ubuntu 24.04 or 22.04.
Prerequisites
- An Ubuntu 22.04 or 24.04 server (VPS, cloud instance, or local VM)
- A user with sudo privileges (do not do this as root)
- Your server's IP address or hostname
- An SSH client: macOS and Linux have
ssh/sftpbuilt in; on Windows use PowerShell's built-in OpenSSH client, WinSCP, or FileZilla
Step 1: Confirm OpenSSH server is installed and running
SFTP is a subsystem of OpenSSH, not a separate daemon, so first make sure the SSH server is present and active:
sudo systemctl status ssh
If the service is active, you are done with this step. If it is missing entirely, install it:
sudo apt update
sudo apt install -y openssh-server
sudo systemctl enable --now ssh
systemctl enable --now starts the service immediately and also starts it automatically after every reboot, so SFTP keeps working after restarts.
Step 2: Create a dedicated SFTP group and user
Never hand out your admin user's credentials for file transfers. Create a separate group for SFTP users and a user that belongs to it:
sudo groupadd sftpusers
sudo useradd -m -G sftpusers -s /usr/sbin/nologin deploy
sudo passwd deploy
What each flag does: -m creates the home directory /home/deploy, -G sftpusers adds the user to the SFTP group, and -s /usr/sbin/nologin sets a shell that can never log in interactively, so this account can only ever do SFTP. passwd sets the account password you will use for the first connection.
Step 3: Lock SFTP users inside a chroot jail
Now the important part. OpenSSH can chroot members of a group into their own home directory with a Match block. First back up the config, then edit it:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo nano /etc/ssh/sshd_config
Scroll to the very bottom of the file and add this block. Match blocks must always go at the end of sshd_config, after every global setting:
Match Group sftpusers
ChrootDirectory %h
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
Line by line: ChrootDirectory %h jails the user inside their own home directory (%h expands to it), so / as seen by the user is really /home/deploy and they cannot cd anywhere above it. ForceCommand internal-sftp overrides the nologin shell problem: it forces every connection from this group to run OpenSSH's built-in SFTP server instead of trying to start a shell. AllowTcpForwarding no and X11Forwarding no strip out forwarding abilities the transfer account does not need.
Step 4: Fix ownership and permissions (the step everyone gets wrong)
A chroot directory has a strict security rule: every component of the chroot path must be owned by root and not writable by anyone else. If /home/deploy is owned by deploy, SSH will refuse the connection with a bad ownership or modes error. Set it up correctly:
sudo chown root:root /home/deploy
sudo chmod 755 /home/deploy
sudo mkdir -p /home/deploy/uploads
sudo chown deploy:sftpusers /home/deploy/uploads
sudo chmod 755 /home/deploy/uploads
The home directory itself is now root-owned and read-only to the user, which satisfies the chroot rule. The uploads subdirectory is owned by the SFTP user, so that is where files actually get written. This two-level layout, root-owned jail with a user-owned folder inside, is the standard pattern. Tell your users to upload into /uploads.
Step 5: Validate the config, restart SSH, and test
Always validate before restarting, a typo here can lock you out of SSH:
sudo sshd -t
sudo systemctl restart ssh
sshd -t prints nothing and exits 0 when the config is valid. Now test from your local machine:
sftp deploy@YOUR_SERVER_IP
Enter the password you set. Once connected, run these checks:
sftp> pwd
Remote working directory: /
sftp> ls
uploads
sftp> cd ..
sftp> pwd
Remote working directory: /
pwd shows /, but that is the jail, really /home/deploy on the server. cd .. goes nowhere: the user is contained. Now test an actual transfer:
sftp> put test.txt /uploads/
sftp> get /uploads/test.txt downloaded.txt
sftp> exit
If both transfers succeed, the jail works end to end.
Step 6 (recommended): Require SSH keys, disable passwords
Password login works, but bots brute-force port 22 constantly. Switch the SFTP group to key-only authentication. On your local machine, generate a key if you do not have one:
ssh-keygen -t ed25519 -f ~/.ssh/deploy_sftp -C "deploy-sftp"
Copy the public key into the jailed user's authorized_keys. Because the home directory is root-owned, do this as root rather than as the user:
sudo mkdir -p /home/deploy/.ssh
sudo cp ~/.ssh/deploy_sftp.pub /home/deploy/.ssh/authorized_keys
sudo chown -R deploy:sftpusers /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
The 700/600 permissions are mandatory: SSH ignores keys in files with loose permissions. Now disable password login for the SFTP group only, by adding one line to your Match block from Step 3:
Match Group sftpusers
ChrootDirectory %h
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
PasswordAuthentication no
A setting inside a Match block overrides the global one, so your admin user keeps its current login method while SFTP users must present a key. Validate and restart again (sudo sshd -t && sudo systemctl restart ssh), then connect with the key:
sftp -i ~/.ssh/deploy_sftp deploy@YOUR_SERVER_IP
Keep a second SSH session logged in as your admin user while you restart sshd. If you ever misconfigure something, that open session is your way back in without a console.
Verify it worked
sftp deploy@YOUR_SERVER_IP(or with-iafter Step 6) connects successfullypwdshows/andcd ..cannot escape it, the jail holdsputinto/uploads/succeeds;putdirectly into/is correctly refused- On the server,
ls -l /home/deploy/uploadsshows your uploaded file owned bydeploy - After Step 6, connecting without the key is rejected even with the right password
Common issues and fixes
1. "Connection refused" or timeouts
Either sshd is not running or a firewall is blocking port 22. Check the service and open the port:
sudo systemctl status ssh
sudo ufw allow ssh
sudo ufw status
If you use a cloud provider (AWS, DigitalOcean, Hetzner), also check the provider's security group or cloud firewall: OS-level UFW rules mean nothing if the provider firewall drops the traffic first.
2. "bad ownership or modes for chroot directory component"
The classic. Some component of the chroot path is not root-owned or is group/world-writable. Fix it exactly as in Step 4:
sudo chown root:root /home/deploy
sudo chmod 755 /home/deploy
Check every level with namei -l /home/deploy; each directory in the path must be owned by root with no write bit for group or others.
3. "Permission denied" when uploading
You are probably trying to write directly into the jail root (/home/deploy), which is intentionally read-only. Upload into the user-owned subdirectory instead: put file.zip /uploads/. If even that fails, verify ownership with ls -ld /home/deploy/uploads, it must show deploy as owner.
4. "Authentication failed" after enabling key-only login
Three usual causes: the public key is not in authorized_keys, the file permissions are wrong, or the client is offering the wrong key. Recheck the 700/600 permissions from Step 6, and watch the server's auth log while you connect to see the exact reason:
sudo tail -f /var/log/auth.log
Then attempt the connection in another terminal. The log line will say precisely what sshd disliked, e.g. bad ownership or modes for file .../authorized_keys.
5. WinSCP/FileZilla: "Received too large SFTP packet" or garbled welcome text
This happens when something in the shell startup prints text (a motd, an echo in .bashrc) before the SFTP protocol starts. The ForceCommand internal-sftp + /usr/sbin/nologin combination from this guide already prevents it. If you still see it, check you did not leave a global Subsystem sftp /usr/lib/openssh/sftp-server line fighting the internal one, and remove any echo lines from the user's shell profile files.
SFTP over a chrooted, key-only account is the right default for client uploads, deploy artifacts, and backups on any Ubuntu server: encrypted, contained, and brute-force resistant. If server hardening, user management, or automated deployments are eating your team's time, XpertBees sets up and manages secure Linux infrastructure for businesses every day, talk to us and we will take it off your plate.



