
Most hosting breaches don’t start with a zero-day. They start with a leaked password.
That password lands on an account that can SSH into your VPS, roam home directories, and upload whatever it wants.
This SFTP setup guide tutorial shows how to create SFTP-only accounts with chroot jails, key-based auth, and sane permissions on Ubuntu, Debian, AlmaLinux, or Rocky. It also keeps the deployment patterns you already rely on.
The goal is simple: clients and developers can transfer files over SFTP. They can’t open a shell. They can’t browse the rest of the filesystem. They can’t overwrite code and configs by accident.
What you’ll build (and what you’ll need)
By the end, you’ll have:
- An
sftpusersgroup with SFTP-only access via OpenSSHinternal-sftp - Optional per-user chroot to
/home/USERNAME(or a custom mount path) - One writable upload directory (and read-only web/app directories if you want)
- Key-based auth (optional but recommended) and a safe password policy fallback
- Logging and quick troubleshooting commands
Prereqs: root SSH access (or sudo), OpenSSH server installed, and a clear target for where your site files live. Common paths are /var/www or /home/USER/public_html on control-panel stacks.
If you’re doing this on production, add guardrails first. Snapshots, monitoring, and a clean restore path reduce the risk of “one bad reload.”
A managed VPS hosting plan or a tuned HostMyCode VPS makes it easier to test changes without stress.
Step 1: Confirm SSHD supports internal-sftp
On current distros, OpenSSH usually includes internal-sftp. Confirm the version and what SSHD thinks its active settings are:
sshd -V 2>&1 | head -n1
sudo sshd -T | grep -E '^(subsystem|passwordauthentication|pubkeyauthentication)'
You want a Subsystem line like Subsystem sftp internal-sftp. You might also see a reference to /usr/lib/openssh/sftp-server.
In this guide, you’ll standardize on internal-sftp. It behaves well inside chroot and keeps the config simpler.
Step 2: Create a dedicated SFTP group
Put SFTP-only accounts in their own group. That keeps policy clean as you add more users.
sudo groupadd --system sftpusers
getent group sftpusers
This also helps on mixed-use servers. You can keep admin/deploy SSH users under one set of rules and clients under another.
Step 3: Create an SFTP-only user (no shell)
This example creates client1 and assigns them to sftpusers.
Set their shell to /usr/sbin/nologin (Debian/Ubuntu) or /sbin/nologin (common on RHEL-family). Use whichever exists on your host.
command -v nologin
# Example (Debian/Ubuntu): /usr/sbin/nologin
sudo useradd -m -g sftpusers -s /usr/sbin/nologin client1
sudo passwd client1
id client1
Setting a password is fine for initial testing.
In Step 7, you’ll switch to keys. At that point, you can also disable passwords for this group.
Step 4: Build a chroot directory layout that won’t fail
Chroot has one hard rule: the chroot directory must be owned by root:root. It also must not be writable by the user.
If you violate that, SFTP refuses to start. You’ll usually see: “bad ownership or modes for chroot directory”.
A reliable pattern is to chroot to the user’s home directory. Then create one writable subfolder for uploads.
# Chroot base (must be root-owned)
sudo chown root:root /home/client1
sudo chmod 755 /home/client1
# Writable directory inside chroot
sudo mkdir -p /home/client1/uploads
sudo chown client1:sftpusers /home/client1/uploads
sudo chmod 750 /home/client1/uploads
Common variations:
- If you need a web root, create
/home/client1/wwwand keep it owned by root, then deploy into it via your CI/CD user (not the client). - If you need the client to manage only
wp-content/uploads, use a bind mount (Step 9) instead of giving broader write access.
Step 5: Update sshd_config for SFTP-only users (Match Group)
Edit your SSH daemon config. It’s typically /etc/ssh/sshd_config.
Some systems prefer drop-ins under /etc/ssh/sshd_config.d/*.conf. If you already manage SSH that way, stick with it. Otherwise, editing the main file is fine.
sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)
sudo nano /etc/ssh/sshd_config
Make sure the SFTP subsystem is set (add it if it’s missing):
Subsystem sftp internal-sftp
Then add this block near the end of the file:
Match Group sftpusers
ChrootDirectory %h
ForceCommand internal-sftp
X11Forwarding no
AllowTcpForwarding no
PermitTTY no
What these lines do:
ChrootDirectory %hkeeps users inside their home directory.ForceCommand internal-sftpblocks shell access even if they try to SSH normally.- Disabling forwarding/TTY trims off extra features you don’t want exposed to client accounts.
Check syntax before you reload anything:
sudo sshd -t
Then reload SSH without dropping existing connections:
sudo systemctl reload ssh
# or: sudo service ssh reload
If you’re hardening SSH more broadly (keys, 2FA, safe rollback), do that first.
The workflow in this SSH lockdown tutorial pairs well with the SFTP setup.
Step 6: Test SFTP access (and confirm shell is blocked)
From your local machine, connect over SFTP:
sftp client1@YOUR_SERVER_IP
You should land inside the chroot and see uploads. Try an upload:
put test.txt uploads/
ls -la
Now try a normal SSH login:
ssh client1@YOUR_SERVER_IP
Expected behavior: no shell. The connection is denied or immediately forces SFTP behavior.
If you still get a shell, your Match Group block isn’t applying. The most common causes are:
- The user isn’t in
sftpusers. - A later/earlier Match block is taking precedence.
Step 7: Switch to SSH keys (recommended) and restrict passwords
Password-only SFTP is an easy target. If you’re giving access to clients, keys are the safer default.
Create the user’s .ssh directory inside the chroot. That’s fine because it lives under /home/client1.
Keep the chroot base itself root-owned. The user should own .ssh and authorized_keys.
sudo mkdir -p /home/client1/.ssh
sudo chown client1:sftpusers /home/client1/.ssh
sudo chmod 700 /home/client1/.ssh
sudo nano /home/client1/.ssh/authorized_keys
sudo chown client1:sftpusers /home/client1/.ssh/authorized_keys
sudo chmod 600 /home/client1/.ssh/authorized_keys
Paste the client’s public key into authorized_keys (one key per line).
Next, decide how aggressive you want to be about passwords. Disabling passwords globally is stronger, but on mixed-use servers it can be disruptive.
A group-scoped rule is a good compromise.
Add this inside the same Match Group sftpusers block:
Match Group sftpusers
PasswordAuthentication no
PubkeyAuthentication yes
ChrootDirectory %h
ForceCommand internal-sftp
X11Forwarding no
AllowTcpForwarding no
PermitTTY no
Reload SSH and test again.
Keep one root session open while you verify. That gives you a way back in if something goes sideways.
Step 8: Tighten logging so you can actually troubleshoot
SFTP problems almost always show up in SSH logs. The usual locations:
- Debian/Ubuntu:
/var/log/auth.log - AlmaLinux/Rocky:
/var/log/secure
sudo tail -n 200 /var/log/auth.log
# or
sudo tail -n 200 /var/log/secure
On busy servers, logs can balloon fast. Make sure log rotation is working so troubleshooting doesn’t become a disk-space incident.
HostMyCode’s logrotate tutorial for hosting logs covers a clean baseline.
Step 9: Optional: Use a bind mount to expose only the folder you want
Sometimes you don’t want the user’s home directory to be their “world.” You want them to see one directory and nothing else.
Bind mounts are a straightforward way to do that.
Example: your WordPress site lives at /var/www/example.com and you only want SFTP access to wp-content/uploads.
1) Create a chroot base and a mount point:
sudo mkdir -p /sftp/client1
sudo chown root:root /sftp/client1
sudo chmod 755 /sftp/client1
sudo mkdir -p /sftp/client1/uploads
sudo chown root:root /sftp/client1/uploads
sudo chmod 755 /sftp/client1/uploads
2) Bind mount the real uploads directory into the chroot:
sudo mount --bind /var/www/example.com/wp-content/uploads /sftp/client1/uploads
3) Make it persistent in /etc/fstab:
sudo nano /etc/fstab
/var/www/example.com/wp-content/uploads /sftp/client1/uploads none bind 0 0
4) Update SSHD to chroot to /sftp/%u for the SFTP group (note: this changes the base path pattern):
Match Group sftpusers
ChrootDirectory /sftp/%u
ForceCommand internal-sftp
X11Forwarding no
AllowTcpForwarding no
PermitTTY no
5) Fix ownership for writable folders. The user must be able to write inside the bind-mounted directory.
In practice, that usually means the underlying directory must be writable by that user (or an appropriate group).
On WordPress hosts, be careful. Broad write permissions can become a long-lived malware foothold.
If you’re unsure, keep uploads writable and keep core code read-only. It’s not perfect, but it’s a sensible default.
Step 10: Add a sane permissions model for web hosting
Chroot helps, but permissions decide what an account can actually change.
On a VPS with multiple sites, that’s the difference between “contained” and “messy.”
- Don’t give an SFTP user write access to the full document root unless you truly need it.
- Do grant write access only to content directories (uploads, cache) and use a separate deploy user (or CI runner) for code.
- Do keep config files (
wp-config.php, environment files, API keys) readable only where required.
Quick diagnostics that catch the usual foot-guns:
# Show ownership and modes
namei -l /home/client1
# Find world-writable directories (a red flag)
sudo find /var/www -type d -perm -0002 -maxdepth 4 2>/dev/null | head
If you run WordPress on a control panel stack, consider built-in isolation instead of recreating everything with manual permissions.
For cPanel servers, the approach in this account isolation tutorial complements SFTP hardening.
Step 11: Firewall and rate-limit basics (to protect SFTP endpoints)
SFTP rides on SSH (port 22 by default). Keep it that way. Don’t open extra ports for “FTP.”
If you need to limit who can connect (office IPs, VPN egress), do it at the firewall.
On cPanel, CSF/LFD is the usual baseline. On a plain Linux VPS, use whatever firewall tooling you already manage.
HostMyCode’s VPS security audit tutorial includes a practical checklist for verifying SSH/firewall exposure without downtime.
A workable policy is to lock admin SSH to known IPs, but leave SFTP reachable if you support remote clients.
If you can’t restrict by IP, enforce keys and run Fail2Ban against your auth logs.
Step 12: Back up what SFTP users can change
Hardening reduces risk. It doesn’t prevent mistakes, compromised keys, or “oops I overwrote the theme.”
Plan for rollback.
At minimum, back up what your SFTP users can write to:
/home/*/uploads(or bind-mounted upload paths)- Any CMS content directories (WordPress uploads, cache if you care)
- Your SSH config:
/etc/ssh/sshd_configand/etc/ssh/sshd_config.d/
For a straightforward recovery path, pair this with incremental rsync backups or the automated approach in nightly encrypted restic backups.
For full-server rollbacks, snapshots are faster; see snapshot backup strategies.
Troubleshooting: common SFTP failures and quick fixes
- “Connection closed” right after login
Check logs for chroot permission errors. Fix with:chown root:root /home/client1 && chmod 755 /home/client1. - “Permission denied” when uploading
Your writable directory isn’t owned by the user. Fix:chown client1:sftpusers /home/client1/uploads. - User still gets SSH shell
They’re not matching the group, or another Match block overrides it. Confirm:id client1, then check sshd config order. - Keys don’t work
Wrong permissions on.ssh/authorized_keys. Use700on.sshand600onauthorized_keys. - Disk fills up from uploads
Uploads directories grow quietly. If you’re low on space, follow this disk space troubleshooting walkthrough and add quotas or cleanup policies.
Operational checklist (copy/paste for change tickets)
- Create group
sftpusers - Create user with
nologinshell - Set chroot base ownership to
root:root, mode755 - Create a writable directory inside chroot (owned by user), mode
750 - Configure
Subsystem sftp internal-sftp - Add
Match Group sftpusersblock withForceCommand internal-sftp - Validate:
sshd -t; reload:systemctl reload ssh - Test SFTP upload + confirm SSH shell is blocked
- Enable keys and disable password auth for the group
- Back up writable paths and test restore
Summary: a safer file-transfer baseline for hosting in 2026
SFTP-only users with chroot and SSH keys give you a predictable, limited file-transfer surface.
Clients can upload what they need. They can’t poke around the server or turn a stolen password into a full shell.
Once you standardize the directory layout and the Match Group rules, onboarding becomes routine instead of risky.
If you want to implement this on a fresh server (or you’re migrating from shared hosting), start with a HostMyCode VPS. If you’d rather hand off the OS baseline, patching, and ongoing upkeep, managed VPS hosting is the simpler route.
SFTP-only accounts are one of the simplest hardening wins for client-friendly hosting. HostMyCode’s VPS plans give you full root control so you can enforce clean OpenSSH policies, while managed VPS hosting makes sense if you want security and maintenance handled day to day.
FAQ: SFTP-only users on a hosting VPS
Is SFTP the same as FTP?
No. SFTP runs over SSH (encrypted end-to-end) and typically uses port 22. FTP is a different protocol and often requires multiple ports and extra hardening.
Can I chroot users without breaking Git-based deployments?
Yes—keep SFTP users for content uploads only, and use a separate deploy user (with limited sudo or a CI runner) for code. Don’t try to make one account do everything.
Why does chroot require root-owned directories?
If the chroot base is user-writable, users can alter the jail itself. OpenSSH blocks that by design. Root ownership is a safety check.
What’s the safest writable directory for WordPress?
Usually wp-content/uploads. Keep core files read-only where possible, and back up uploads frequently.
Should I disable password authentication entirely?
On most production VPS and dedicated servers in 2026, yes—at least for exposed SSH. If you must keep passwords, combine strong passwords with rate-limiting and monitoring.