Back to tutorials
Tutorial

SFTP Setup Guide Tutorial (2026): Secure File Transfers for a VPS with SSH Keys, Chroot, and Safe Permissions

SFTP setup guide tutorial for 2026: lock down SSH, chroot users, and fix permissions for safe uploads on your VPS.

By Anurag Singh
Updated on Jun 10, 2026
Category: Tutorial
Share article
SFTP Setup Guide Tutorial (2026): Secure File Transfers for a VPS with SSH Keys, Chroot, and Safe Permissions

SFTP feels “done” until you hand access to a developer, client, or contractor. Then the sharp edges show up. Users land in the wrong directory. Web roots become writable. Shells slip through. Permissions either block WordPress updates or expose config files.

This SFTP setup guide tutorial walks through a production-friendly SFTP setup on an Ubuntu VPS in 2026. You’ll use SSH keys by default, a chrooted upload area, and no shell access. You’ll also set permissions that work with common stacks (Nginx/Apache + PHP-FPM). Finally, you’ll get quick checks for the usual “permission denied” and “connection refused” failures.

What you’ll build (and why this structure avoids common hosting mistakes)

By the end, you’ll have:

  • A dedicated sftp group for file-transfer-only users
  • Users jailed into /home/<user> (chroot) so they can’t browse your server
  • An upload/edit directory inside the jail that is writable without making the chroot itself writable
  • SSH key authentication (and optional password fallback with guardrails)
  • Locked-down SSH settings so SFTP users can’t open an interactive shell or port-forward

This pattern beats “just give them an SSH account.” It limits what a compromised credential can touch.

It also keeps audits cleaner. It prevents edits outside the intended site directory.

Prerequisites on your VPS

  • Ubuntu 24.04 LTS or Ubuntu 22.04 LTS (commands below assume Ubuntu; Debian is very similar)
  • Root access or a sudo-enabled admin user
  • OpenSSH Server installed (most VPS images already include it)

If you’re provisioning a new server, starting clean helps.

If you want a ready-to-run environment with a consistent security baseline, start with a HostMyCode VPS. If you’d rather keep maintenance off your plate, use managed VPS hosting.

SFTP setup guide tutorial: Install and verify OpenSSH

First, confirm SSH is installed and running:

sudo apt update
sudo apt install -y openssh-server
sudo systemctl enable --now ssh
sudo systemctl status ssh --no-pager

Then check which port sshd is listening on:

sudo ss -lntp | grep sshd

Most servers use port 22. If you changed it, write it down.

You’ll need the port for firewall rules and client settings.

Create an SFTP-only group and user

Create a group that represents “SFTP-only” accounts:

sudo groupadd --system sftpusers

Next, create the user and add them to that group.

Use /usr/sbin/nologin so they can’t get a shell. This still helps even if your SSH config changes later.

sudo useradd \
  --create-home \
  --home-dir /home/client1 \
  --shell /usr/sbin/nologin \
  --groups sftpusers \
  client1

If you need password auth temporarily (for example, a client can’t use keys), set a password:

sudo passwd client1

Treat password access as a short-lived exception. In 2026, keys should be your normal path.

Build a safe chroot directory layout (the chroot must not be writable)

Chroot rules are unforgiving. The chroot directory (the jail root) must be owned by root.

It also must not be writable by the user.

The standard fix is simple. Keep the jail root locked down. Put a writable directory inside it.

We’ll use this layout:

  • /home/client1 (owned by root:root, not writable by client1) — chroot root
  • /home/client1/files (owned by client1:sftpusers, writable) — where uploads happen
sudo chown root:root /home/client1
sudo chmod 0755 /home/client1

sudo mkdir -p /home/client1/files
sudo chown client1:sftpusers /home/client1/files
sudo chmod 0750 /home/client1/files

Common pitfall: If you make /home/client1 owned by client1, OpenSSH will reject the session with “bad ownership or modes for chroot directory”.

Configure sshd for SFTP-only access (Match block + internal-sftp)

OpenSSH can serve SFTP without providing a shell. To do that, use internal-sftp.

Edit:

sudo nano /etc/ssh/sshd_config

Make sure the SFTP subsystem is set (it often is already):

Subsystem sftp internal-sftp

Then add this Match block at the very end of the file.

These settings apply only to members of your SFTP group:

Match Group sftpusers
  ChrootDirectory %h
  ForceCommand internal-sftp
  X11Forwarding no
  AllowTcpForwarding no
  PermitTunnel no
  GatewayPorts no
  PermitTTY no
  PasswordAuthentication no

Notes:

  • ChrootDirectory %h uses the user’s home as the jail root (here: /home/client1).
  • PasswordAuthentication no keeps SFTP users on keys only. You can add a narrow exception later if you have to.
  • AllowTcpForwarding no prevents the “SFTP account turned into a tunnel” scenario.

Validate the config and reload SSH safely:

sudo sshd -t
sudo systemctl reload ssh

If sshd -t reports an error, fix it before reloading.

A bad sshd_config can lock you out.

Add SSH keys for the SFTP user (recommended default)

Usually you’ll receive a public key from the user (ends in .pub).

You can also generate one for them.

On the user’s local machine (not the server), they can run:

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

On the VPS, create .ssh and authorized_keys.

Even with chroot, OpenSSH reads keys before the chroot takes effect. That’s why you still place them under the user’s home directory.

sudo mkdir -p /home/client1/.ssh
sudo chmod 0700 /home/client1/.ssh
sudo chown client1:sftpusers /home/client1/.ssh

sudo nano /home/client1/.ssh/authorized_keys

Paste the public key as a single line. Then tighten permissions:

sudo chmod 0600 /home/client1/.ssh/authorized_keys
sudo chown client1:sftpusers /home/client1/.ssh/authorized_keys

Important: The jail root must be root-owned. Subdirectories like .ssh and files can be user-owned.

That’s the expected setup.

Test login and confirm the jail works

From your local machine:

sftp -i ~/.ssh/client1_sftp client1@YOUR_SERVER_IP

After login, run:

sftp> pwd
sftp> ls
sftp> cd files
sftp> put test.txt

You should not be able to navigate outside the jail root.

If you can browse the real filesystem (for example /etc), your chroot isn’t being applied.

Open firewall rules for SFTP (and avoid exposing anything else)

If you use UFW, allow your SSH port:

sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verbose

If SSH runs on a custom port (say 2222), allow that instead:

sudo ufw allow 2222/tcp

For a production ruleset with iptables (useful on dedicated servers and multi-service VPS setups), see our production iptables web-hosting ruleset tutorial.

Set correct permissions for web hosting uploads (WordPress and PHP sites)

SFTP often becomes the “site files” pipeline. Permission problems show up fast, especially when WordPress also needs to write.

Two common patterns:

  • Single-owner workflow: The SFTP user owns site files; the web server only needs read access. Best when you deploy via SFTP/Git and keep WordPress updates controlled.
  • Shared-group workflow: The SFTP user and web server share a group so both can write (useful for WordPress plugin/theme updates via wp-admin, but higher risk).

Option A: safer default (web server read-only)

Keep WordPress core and config locked down.

Do updates through SFTP (or a proper deploy). Avoid updates through the WordPress UI.

  • Directories: 0755
  • Files: 0644
  • wp-config.php: 0640 or 0600 depending on your setup

Example (adjust paths):

sudo find /var/www/example.com/public -type d -exec chmod 0755 {} \;
sudo find /var/www/example.com/public -type f -exec chmod 0644 {} \;
sudo chmod 0640 /var/www/example.com/public/wp-config.php

Option B: shared-group workflow (controlled write access)

If WordPress must write to wp-content (uploads, cache, and some updates), confine write access to the wp-content subtree.

On Ubuntu, the web server user is usually www-data. Add it to the SFTP group.

Then set group ownership on wp-content:

sudo usermod -aG sftpusers www-data
sudo chgrp -R sftpusers /var/www/example.com/public/wp-content
sudo chmod -R g+rwX /var/www/example.com/public/wp-content
sudo find /var/www/example.com/public/wp-content -type d -exec chmod 2775 {} \;

The 2775 sets the setgid bit so new files inherit the group.

This prevents the “SFTP upload breaks WordPress media permissions” loop.

Harden SSH globally (without breaking SFTP)

Your Match block constrains SFTP users. Your admin access needs the same attention.

  • Disable root login
  • Use keys for admins
  • Restrict who can SSH

We cover the full admin-side posture (keys, allowlists, optional 2FA, audit logs) in our SSH lockdown tutorial for hosting VPS.

It pairs well with SFTP because both run through the same daemon.

Turn on Fail2Ban for SSH/SFTP brute-force protection

SFTP endpoints get constant background noise from bots. Even with passwords disabled, you’ll still see hammering and log spam.

Install Fail2Ban:

sudo apt update
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban

For a solid baseline jail config (SSH plus WordPress-related endpoints), follow our Fail2Ban setup tutorial.

If you’re only using SFTP, focus on the sshd jail.

Also make sure your admin IPs won’t get caught by mistake.

Optional: Allow passwords for one user (without opening the floodgates)

Sometimes you inherit a client with legacy tooling. If you must allow password authentication, scope it tightly.

Keep it temporary.

One approach is to keep PasswordAuthentication no globally. Then add a separate group and a narrow Match block for the exception. Example:

sudo groupadd --system sftp_pw_temp
sudo usermod -aG sftp_pw_temp client1

Then in /etc/ssh/sshd_config (below your main SFTP block):

Match Group sftp_pw_temp
  PasswordAuthentication yes

Reload SSH and set a calendar reminder to remove the user from that group.

Quick troubleshooting checklist (the errors you’ll actually see)

Most SFTP failures trace back to three causes: chroot ownership, key permissions, or a port/firewall mismatch.

1) “Connection refused” or timeout

  • Confirm SSH is listening: sudo ss -lntp | grep sshd
  • Confirm firewall allows the port: sudo ufw status
  • Check provider security group / upstream firewall (if applicable)

For slow or flaky connections on a loaded VPS, use our metrics-first workflow in the VPS troubleshooting tutorial.

SSH issues often correlate with CPU steal, IO wait, or saturated storage.

2) “bad ownership or modes for chroot directory”

Fix the jail root directory:

sudo chown root:root /home/client1
sudo chmod 0755 /home/client1

Then make sure the writable directory lives inside the jail:

sudo chown client1:sftpusers /home/client1/files
sudo chmod 0750 /home/client1/files

3) “Permission denied (publickey)”

  • Verify the right key is in /home/client1/.ssh/authorized_keys
  • Check permissions: .ssh should be 0700, authorized_keys should be 0600
  • Confirm ownership: client1:sftpusers
sudo ls -ld /home/client1 /home/client1/.ssh
sudo ls -l /home/client1/.ssh/authorized_keys

For more detail while testing:

sudo journalctl -u ssh -n 200 --no-pager

4) User gets a shell instead of SFTP

  • Confirm they are in the right group: id client1
  • Confirm the Match block is at the end of sshd_config
  • Confirm ForceCommand internal-sftp is inside the Match block

Operational habits that keep SFTP from becoming a liability

  • One user per person. Shared logins destroy accountability and make offboarding messy.
  • Keys expire. Rotate keys when contractors roll off, and remove their user right away.
  • Log reviews. Check SSH auth logs weekly, or use automated summaries.

If you want daily summaries delivered by email (auth failures, service restarts, disk warnings), set up Logwatch on an Ubuntu hosting VPS.

It’s simple, but it surfaces issues early.

Summary: a production-safe SFTP baseline for hosting in 2026

A good SFTP setup is mostly about constraints. Chroot users. Remove shell access. Default to keys.

Limit write access to the few directories that truly need it.

With those guardrails in place, SFTP is a reliable way to move site files.

It won’t turn your VPS into a shared jump box.

If you’re rolling this out across multiple sites or client accounts, use a VPS plan sized for steady IO and growth.

A HostMyCode VPS gives you the control to enforce SSH/SFTP policies consistently, and managed VPS hosting is a practical choice if you want the same results with less admin overhead.

If you’re setting up SFTP for client sites, pick hosting that won’t block you on SSH access, firewall rules, or clean server baselines. Start with a HostMyCode VPS for full control, or use managed VPS hosting if you want HostMyCode handling the underlying server maintenance while you manage sites and users.

FAQ

Is SFTP the same as FTPS?

No. SFTP runs over SSH (one service, one port). FTPS is FTP over TLS (separate protocol, more firewall complexity).

For VPS hosting admin, SFTP is usually simpler and easier to audit.

Can I chroot SFTP users and still let them upload to /var/www?

Yes, but avoid bind-mounting the entire /var/www unless you’re confident in permissions.

A common compromise is to bind-mount a single site directory into /home/user/files and keep the chroot root owned by root.

What’s the safest way to handle WordPress uploads via SFTP?

Give SFTP write access to wp-content (or a staging area) and keep wp-config.php and core files locked down.

If WordPress needs to write, scope it to wp-content only.

Why do my SFTP users get “permission denied” even though the folder is writable?

Usually it’s one of: wrong ownership, missing execute bit on parent directories, or an SELinux/AppArmor policy (less common on Ubuntu).

Start by checking ls -ld on every directory in the path.

How do I offboard a contractor quickly?

Lock the account and remove keys:

sudo usermod -L client1
sudo truncate -s 0 /home/client1/.ssh/authorized_keys

Then archive their files and delete the user when you’re sure nothing is needed.