Back to tutorials
Tutorial

Set Up SFTP-Only Users on Ubuntu VPS

Set up SFTP-only users on an Ubuntu VPS with chroot, per-site folders, and safe permissions—no SSH shell access required.

By Anurag Singh
Updated on Oct 22, 2026
Category: Tutorial
Share article
Set Up SFTP-Only Users on Ubuntu VPS

“SFTP access” often turns into “oops, they can SSH into the box.” It can also expose more of the filesystem than you intended.

This walkthrough shows you how to set up SFTP-only users on an Ubuntu VPS using OpenSSH’s internal-sftp, chroot jails, and permissions that won’t break real web apps. It’s aimed at multi-site admins who need safe uploads without giving out a shell.

This works on any Ubuntu VPS you control (20.04/22.04/24.04 LTS are all fine in 2026).

If you host multiple sites and want clean isolation, start with a HostMyCode VPS. If you’d rather not babysit patching and SSH hardening, managed VPS hosting keeps that workload off your plate.

What you’ll build (and what “SFTP-only” really means)

By the end you’ll have:

  • A dedicated Unix group for SFTP users.
  • Accounts that can authenticate with SFTP but cannot open an interactive shell.
  • A per-site chroot jail so each user sees only their own directory tree.
  • Write access where it belongs (an uploads/ directory, plus optional releases/ or public/ areas) without making your whole web root writable.

One key detail: SFTP isn’t a separate daemon. It’s an SSH subsystem.

You still rely on sshd, keys, and your SSH security controls. “Only” is enforced by configuration: internal-sftp + chroot + no TTY/shell.

Prerequisites and quick safety checks

  • Ubuntu server with OpenSSH installed (most VPS images include it).
  • Root access (or sudo).
  • At least one existing site directory (example: /var/www/site1).
  • A second SSH session open while you edit sshd config. Don’t skip this.

Confirm OpenSSH is installed and running:

ssh -V
systemctl status ssh --no-pager

Confirm the SFTP subsystem is available (it’s enabled by default on Ubuntu):

sshd -T | grep -i subsystem

If you also want broader SSH lockdown (keys only, no root login), do it as a separate change. Apply one change at a time. Test each change carefully.

For a structured baseline, pair this with: Set Up UFW Firewall on Ubuntu for a Web Server.

Create an SFTP-only group and a no-shell user

Start by creating a group you’ll target with a Match Group rule in sshd:

sudo addgroup sftpusers

Now create a user with no interactive shell. Ubuntu’s /usr/sbin/nologin is designed for this:

sudo adduser --shell /usr/sbin/nologin --disabled-password sftp_site1
sudo usermod -aG sftpusers sftp_site1

Only set a password if you truly need password-based SFTP. It’s usually a bad tradeoff.

Keys are simpler. They also reduce brute-force exposure. If you must set a password:

sudo passwd sftp_site1

Recommendation: use SSH keys for these accounts. You can still enforce SFTP-only with key auth.

Build the chroot directory layout (the part that breaks most setups)

OpenSSH chroot rules are strict.

The chroot directory (and every directory leading to it) must be owned by root and not writable by the SFTP user. If you miss this, you’ll often see a clean disconnect right after login.

Here’s the layout we’ll use for site1:

  • /var/sftp/site1 (chroot root, owned by root)
  • /var/sftp/site1/www (root-owned container directory)
  • /var/sftp/site1/www/uploads (writable by the SFTP user)

Create the directories and set ownership/permissions:

sudo mkdir -p /var/sftp/site1/www/uploads
sudo chown root:root /var/sftp/site1
sudo chmod 755 /var/sftp/site1

sudo chown root:root /var/sftp/site1/www
sudo chmod 755 /var/sftp/site1/www

sudo chown sftp_site1:sftpusers /var/sftp/site1/www/uploads
sudo chmod 775 /var/sftp/site1/www/uploads

Verification:

namei -l /var/sftp/site1/www/uploads

You’re looking for root:root ownership on /var, /var/sftp, /var/sftp/site1, and /var/sftp/site1/www.

Only the final writable directory (like uploads) should be owned by the SFTP user.

Wire the chroot to your real website directory (two practical options)

Uploads must be visible to your web server and app. Two patterns work well.

Pick one pattern and use it everywhere. Consistency matters on multi-site servers.

Option A (recommended): bind-mount the real web uploads directory into the chroot

This keeps your canonical site path unchanged. That matters for WordPress, Laravel, and many custom deployments.

Example: the live site is /var/www/site1, and uploads live at /var/www/site1/public/uploads.

Create the real uploads directory if it doesn’t exist:

sudo mkdir -p /var/www/site1/public/uploads
sudo chown -R www-data:www-data /var/www/site1/public

Bind-mount it into the SFTP jail:

sudo mount --bind /var/www/site1/public/uploads /var/sftp/site1/www/uploads

Persist it across reboots with an /etc/fstab entry:

echo '/var/www/site1/public/uploads /var/sftp/site1/www/uploads none bind 0 0' | sudo tee -a /etc/fstab

Verify the mount is in place:

findmnt /var/sftp/site1/www/uploads
mount | grep /var/sftp/site1/www/uploads

Permissions note: if your web server runs as www-data, you often want both www-data and the SFTP user to write to uploads.

A practical approach is a shared group plus setgid on the uploads directory.

Create a shared group and add both identities:

sudo addgroup site1files
sudo usermod -aG site1files sftp_site1
sudo usermod -aG site1files www-data

sudo chown -R root:site1files /var/www/site1/public/uploads
sudo chmod 2775 /var/www/site1/public/uploads

Optional but useful: default ACLs so new files inherit group write:

sudo apt-get update
sudo apt-get install -y acl
sudo setfacl -R -m g:site1files:rwx /var/www/site1/public/uploads
sudo setfacl -R -d -m g:site1files:rwx /var/www/site1/public/uploads

Option B: keep uploads inside the chroot and point your app there

This is mechanically simpler.

However, your app must use /var/sftp/site1/www/uploads as its uploads path, or you’ll need a symlink from the web root.

For WordPress, that’s usually extra friction. For custom apps, it can be perfectly workable.

Edit sshd_config to force internal-sftp and chroot per user

Back up your SSH config before you touch it:

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)

Edit /etc/ssh/sshd_config:

sudo nano /etc/ssh/sshd_config

Make sure you have this line (Ubuntu typically does).

If you already have a Subsystem line, keep a single copy and avoid duplicates:

Subsystem sftp internal-sftp

Then add a match block at the bottom of the file:

Match Group sftpusers
    ChrootDirectory /var/sftp/%u
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no
    PermitTTY no

This configuration expects each user’s chroot at /var/sftp/USERNAME. For sftp_site1, that means:

/var/sftp/sftp_site1

Earlier, we used /var/sftp/site1. To reconcile that, either:

  • Rename the directory to match the username, or
  • Use a per-user Match block with a fixed ChrootDirectory.

If you prefer “site name” folders, a per-user block is usually the cleanest approach. Example:

Match User sftp_site1
    ChrootDirectory /var/sftp/site1
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no
    PermitTTY no

Choose one approach and stick to it.

On multi-site servers, per-user blocks mapped to site folders are easier to understand later.

Validate the config before restarting SSH:

sudo sshd -t

If you get no output, syntax is fine. Restart SSH:

sudo systemctl restart ssh

Keep your existing SSH session open until you confirm your admin login still works.

Create the chroot folder for the user (if using %u) and retest permissions

If you used ChrootDirectory /var/sftp/%u, build that structure now:

sudo mkdir -p /var/sftp/sftp_site1/www/uploads
sudo chown root:root /var/sftp/sftp_site1 /var/sftp/sftp_site1/www
sudo chmod 755 /var/sftp/sftp_site1 /var/sftp/sftp_site1/www
sudo chown sftp_site1:sftpusers /var/sftp/sftp_site1/www/uploads
sudo chmod 775 /var/sftp/sftp_site1/www/uploads

If you’re bind-mounting the real uploads directory, mount it to the new path:

sudo mount --bind /var/www/site1/public/uploads /var/sftp/sftp_site1/www/uploads
echo '/var/www/site1/public/uploads /var/sftp/sftp_site1/www/uploads none bind 0 0' | sudo tee -a /etc/fstab

Verify the chroot rule won’t reject the path:

namei -l /var/sftp/sftp_site1/www/uploads

Add SSH key authentication for the SFTP-only user (preferred)

On your local machine, generate a key if you don’t already have one:

ssh-keygen -t ed25519 -a 64 -f ~/.ssh/sftp_site1

Next, install the public key on the server.

Because the session gets chrooted, the simplest setup is to keep authorized_keys in the user’s home directory (outside the jail). sshd reads it before applying the chroot.

Create the user’s .ssh directory:

sudo mkdir -p /home/sftp_site1/.ssh
sudo chmod 700 /home/sftp_site1/.ssh
sudo chown -R sftp_site1:sftp_site1 /home/sftp_site1/.ssh

Append the public key (adjust the path to match your machine):

cat ~/.ssh/sftp_site1.pub | sudo tee -a /home/sftp_site1/.ssh/authorized_keys > /dev/null
sudo chmod 600 /home/sftp_site1/.ssh/authorized_keys
sudo chown sftp_site1:sftp_site1 /home/sftp_site1/.ssh/authorized_keys

Optional but smart: restrict the key so it’s only usable for SFTP (defense in depth). Prepend options to the key line:

sudo nano /home/sftp_site1/.ssh/authorized_keys

Change the line to:

no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAA... your-key-comment

Test: verify SFTP works and SSH shell does not

From your local machine, start an SFTP session:

sftp -i ~/.ssh/sftp_site1 sftp_site1@YOUR_SERVER_IP

You should land inside the chroot. Run a quick sanity check:

pwd
ls -la
cd /www/uploads
put test.txt
ls -la

Now try SSH:

ssh -i ~/.ssh/sftp_site1 sftp_site1@YOUR_SERVER_IP

Expected result: no shell prompt.

You’ll usually see “This service allows sftp connections only,” or the session closes immediately. Either outcome is fine.

Common errors and fast fixes (with exact diagnostics)

“Connection closed” immediately after SFTP login

This is almost always a chroot ownership/permission violation.

  • The chroot directory must be owned by root:root.
  • The chroot directory must not be group- or world-writable.

Run:

sudo namei -l /var/sftp/site1
sudo journalctl -u ssh -n 80 --no-pager

In the logs, look for bad ownership or modes for chroot directory.

“Permission denied” when uploading into uploads/

Fix the writable directory ownership:

sudo chown sftp_site1:sftpusers /var/sftp/site1/www/uploads
sudo chmod 775 /var/sftp/site1/www/uploads

If you’re using bind mounts, check the real directory too:

ls -ld /var/www/site1/public/uploads
id sftp_site1
id www-data

Bind mount disappears after reboot

Your /etc/fstab entry is missing or wrong. Confirm what’s in place, then re-mount:

grep uploads /etc/fstab
sudo mount -a
findmnt | grep sftp

You accidentally locked yourself out of SSH

This is almost always a broken sshd_config edit.

Prevent it by running sshd -t before restarting.

If you’re already locked out, use your VPS provider console (VNC/serial/rescue) and restore the backup:

sudo cp /etc/ssh/sshd_config.bak.YYYY-MM-DD /etc/ssh/sshd_config
sudo sshd -t
sudo systemctl restart ssh

Make it multi-site: repeatable pattern for many tenants

On multi-tenant servers, consistency keeps you out of trouble.

Use the same pattern per site:

  1. Create user: adduser --shell /usr/sbin/nologin --disabled-password
  2. Add to group: usermod -aG sftpusers
  3. Create chroot: /var/sftp/SITENAME owned by root
  4. Create writable dir inside: /www/uploads owned by user
  5. (Recommended) bind-mount real site uploads into jail
  6. Install SSH key in /home/USER/.ssh/authorized_keys
  7. Test SFTP upload + confirm SSH shell blocked

If you regularly move sites between providers, plan DNS cutovers and SSL renewal ahead of time. This guide walks through a clean approach: DNS cutover tutorial for zero-downtime migrations.

Hardening extras that fit SFTP-only accounts

None of these are required for basic functionality. They do reduce risk on an internet-facing SSH service.

  • Disable password auth for SFTP users: keep those accounts key-only (globally or via match blocks).
  • Rate-limit SSH at the firewall: UFW helps; for higher volume, enforce policies with nftables.
  • Keep uploads out of executable paths: don’t make the full web root writable—limit writes to uploads/.
  • Monitor auth logs: repeated failures and unknown usernames should stand out quickly.

To disable password auth only for the SFTP group, add this inside the same match block:

Match Group sftpusers
    PasswordAuthentication no
    PubkeyAuthentication yes
    AuthenticationMethods publickey

Then validate and restart:

sudo sshd -t
sudo systemctl restart ssh

To watch what happens during a test login:

sudo tail -f /var/log/auth.log

Backups: don’t stop at “we have backups,” test restores

SFTP-only users often touch the hardest data to recreate: uploads, media, and customer documents.

Back up both the database and the uploads directory. Then prove you can restore them.

If you want a repeatable restore-test routine, use: VPS Backup Verification Tutorial (2026).

If your sites run on MariaDB, this companion guide covers dumps and storage hygiene: Install MariaDB and create secure backups on Ubuntu.

Summary: a safe upload path without handing out shells

To set up SFTP-only users on Ubuntu without surprises, align three pieces: forced internal-sftp, a root-owned chroot directory, and a separate writable directory inside the jail.

In practice, that writable directory is often bind-mounted to the site’s real uploads path.

Once you standardize the layout, adding a new site user is quick. You also stop granting full SSH access just to receive files.

If you’re consolidating multiple sites onto one server, run this setup on a HostMyCode VPS for predictable isolation. If you’re pushing large media libraries or doing frequent deployments, dedicated servers can give you more I/O headroom.

If you host multiple client sites, clean user isolation is the difference between “safe uploads” and “shared server access.” Use a VPS you control to keep SFTP accounts locked to the directories they need. Start with HostMyCode VPS, or choose managed VPS hosting if you want us to handle OS updates and SSH maintenance while you focus on the sites.

FAQ: SFTP-only accounts on an Ubuntu VPS

Can an SFTP-only user still run commands on the server?

Not if you use ForceCommand internal-sftp and PermitTTY no. They authenticate through sshd, and sshd places them directly into the SFTP subsystem.

Why does OpenSSH require the chroot folder to be root-owned?

If the user could write to the chroot root, they could modify the jail environment. OpenSSH enforces root ownership so the jail boundary stays trustworthy.

Should I use passwords for SFTP accounts?

Keys are safer and easier to rotate. If you must use passwords, pair them with strong rate limiting and monitoring. For most teams in 2026, keys are the default.

What’s the safest place for uploads in a web app?

A dedicated uploads directory that isn’t executable, isn’t your entire document root, and is writable only where necessary. A chrooted SFTP account should write there—and nowhere else.

How do I give one user access to two site folders?

Chroot makes “two separate roots” awkward. The clean approach is one user per site.

If you must share, create a single chroot that contains both sites as subfolders, then manage permissions carefully.

Set Up SFTP-Only Users on Ubuntu VPS | HostMyCode