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/sftp built 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 -i after Step 6) connects successfully
  • pwd shows / and cd .. cannot escape it, the jail holds
  • put into /uploads/ succeeds; put directly into / is correctly refused
  • On the server, ls -l /home/deploy/uploads shows your uploaded file owned by deploy
  • 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.