Back to tutorials
Tutorial

VPS SFTP Setup Guide Tutorial (2026): Secure File Transfers with Chroot Jails, SSH Hardening, and Per-User Limits

VPS SFTP setup guide tutorial for secure uploads: chroot SFTP, per-user keys, limits, and safe permissions on Linux (2026).

By Anurag Singh
Updated on Oct 04, 2026
Category: Tutorial
Share article
VPS SFTP Setup Guide Tutorial (2026): Secure File Transfers with Chroot Jails, SSH Hardening, and Per-User Limits

Most file-transfer headaches on hosting servers come down to one choice you control: do clients get a real shell, or do they get SFTP only?

A properly jailed SFTP account prevents the classic “oops, I deleted /var/www” moment. It also limits damage from reused credentials and keeps your VPS tidy.

This VPS SFTP setup guide tutorial walks through a production-ready setup for Ubuntu/Debian and AlmaLinux/Rocky. You’ll use chroot jails, per-user keys, and practical limits that hold up in day-to-day operations.

If you’re doing this on a new server, start from a stable baseline. A small hosting VPS works for a few sites.

Agencies and resellers usually need predictable I/O and extra headroom.

HostMyCode’s HostMyCode VPS plans fit SFTP-heavy workflows. You control SSH, firewall rules, and per-user permissions without fighting the platform.

What you’ll build (and why it works)

You’ll configure OpenSSH so a set of users (usually a group) can use SFTP and nothing else. They’ll be chrooted into a directory tree you own, with no interactive shell access.

  • SFTP-only users: no SSH shell, no port forwarding, less opportunity for surprises.
  • Chroot jail: users can’t browse outside the directory you assign.
  • Per-user keys: no shared passwords; revoke access by removing a single key.
  • Guardrails: sensible limits and logs you can use when something breaks.

Prerequisites and baseline checks

Before editing sshd, keep a live root session open. Keep a second way back in too (provider console, out-of-band access, or at least a second SSH session).

SSH lockouts are common during “quick” changes.

  • A VPS or dedicated server with root access
  • OpenSSH server installed (usually already present)
  • A domain is optional for SFTP, but useful if you prefer DNS hostnames

Identify your OS family

cat /etc/os-release
sshd -V 2>&1 | head -n1

On Ubuntu/Debian, the config is typically /etc/ssh/sshd_config. On AlmaLinux/Rocky, it’s the same file.

SELinux can still affect chrooted access.

Back up SSH config before editing

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

If your VPS already has “random” access issues, fix the basics first. Start with time sync, firewall rules, and consistent hostnames.

These guides cover the usual culprits: VPS time sync troubleshooting and VPS firewall setup guide.

Step 1: Create an SFTP-only group and directory layout

Put SFTP users in a dedicated group. This keeps your sshd_config clean.

It also makes onboarding easier. You can add or remove users without revisiting SSH policy every time.

Create a group

groupadd sftpusers

Pick a jail root path

A common layout uses /sftp as the jail root, with one subdirectory per user.

The chroot directory must be owned by root and not writable by the user. If it’s writable, OpenSSH will refuse the login.

mkdir -p /sftp
chown root:root /sftp
chmod 755 /sftp

Create a per-user jail and upload directory

Example for a user named client1:

useradd -m -g sftpusers -s /usr/sbin/nologin client1
mkdir -p /sftp/client1/upload
chown root:root /sftp/client1
chmod 755 /sftp/client1
chown client1:sftpusers /sftp/client1/upload
chmod 750 /sftp/client1/upload

Why two directories? /sftp/client1 is the chroot root, so it must be root-owned. /sftp/client1/upload is the writable area for file transfers.

Optional: map to a website document root

If the goal is “upload to WordPress” or “deploy static files,” you can bind-mount a site directory into the jail.

Clients get a familiar path (similar to public_html) without broader filesystem access.

# Example: bind mount /var/www/site1 into the jail
mkdir -p /sftp/client1/site1
mount --bind /var/www/site1 /sftp/client1/site1

Make it persistent (Ubuntu/Debian/AlmaLinux/Rocky) by adding an /etc/fstab entry:

/var/www/site1  /sftp/client1/site1  none  bind  0  0

Pitfall: if you bind-mount a writable directory, clients can change live code. That may be acceptable for agency workflows.

For shared hosting, it often isn’t. A safer pattern is “upload to upload/” and deploy via a controlled process.

Step 2: Configure sshd for chrooted SFTP-only access

You’ll use Match Group to apply restrictions only to SFTP users.

Admins and automation accounts keep normal SSH behavior.

Edit sshd_config

Open /etc/ssh/sshd_config and confirm this line exists (it often does):

Subsystem sftp internal-sftp

Then add this block at the bottom of the file:

Match Group sftpusers
  ChrootDirectory /sftp/%u
  ForceCommand internal-sftp
  X11Forwarding no
  AllowTcpForwarding no
  PermitTunnel no
  GatewayPorts no
  PasswordAuthentication no

Notes:

  • ChrootDirectory /sftp/%u expects /sftp/client1, /sftp/client2, etc.
  • PasswordAuthentication no inside the Match block forces keys for this group, without changing global SSH policy.
  • If other users still need passwords, leave global password auth unchanged.

Validate config and reload safely

sshd -t
systemctl reload ssh || systemctl reload sshd

If sshd -t produces no output, the syntax is valid. If you get an error, fix it before reloading.

Step 3: Add SSH keys per user (no passwords)

Key-only SFTP access is easier to audit and easier to revoke. It also avoids the “same password everywhere” problem that shows up constantly in client environments.

Create .ssh and authorized_keys inside the user’s home

Even with chrooting, OpenSSH reads authorized_keys from the user’s home by default.

For the user created with -m, that home is likely /home/client1.

mkdir -p /home/client1/.ssh
chmod 700 /home/client1/.ssh
touch /home/client1/.ssh/authorized_keys
chmod 600 /home/client1/.ssh/authorized_keys
chown -R client1:sftpusers /home/client1/.ssh

Paste the client public key

On the server:

nano /home/client1/.ssh/authorized_keys

On the client machine, generate a modern key like this:

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

Test login (from your workstation)

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

After you connect, verify the user only sees the jail contents. Then confirm they can write only in upload/ (or your bind-mounted path).

Step 4: Apply practical limits (avoid noisy neighbors and accidental floods)

SFTP rides on SSH, so one user can still burn CPU, disk, or inodes with large uploads or too many files.

Limits won’t fix a broken application. They do shrink the blast radius.

Option A: Per-user connection limits in sshd

Add these globally (outside Match blocks) if you want conservative defaults:

MaxAuthTries 3
MaxSessions 10
LoginGraceTime 30

Interpretation: MaxSessions caps multiplexed sessions per network connection. If your developers use ControlMaster heavily, don’t set it too low.

Option B: System resource limits via systemd (recommended on modern distros)

OpenSSH typically runs as a systemd service. You can constrain it with a drop-in override.

systemctl edit ssh.service || systemctl edit sshd.service

Add:

[Service]
# Prevent pathological fork bombs through SSH
TasksMax=4096
# Conservative CPU accounting for bursts; adjust for your box
CPUAccounting=true
MemoryAccounting=true

Then:

systemctl daemon-reload
systemctl restart ssh || systemctl restart sshd

Option C: Disk quotas for jailed upload directories

If clients upload backups or media, quotas help prevent the “disk fills at 3am” ticket.

Exact steps depend on your filesystem and mount layout. Use this as a checklist:

  • Confirm where /sftp lives (df -hT /sftp).
  • If it’s on /, quotas impact your root filesystem; consider a separate volume for uploads on serious hosting nodes.
  • Enable user quotas on that filesystem, then set a per-user limit.

While you’re tightening access, keep log growth in check. A runaway log can fill /var and break authentication.

That failure often looks like “SSH is down.”

Use this guide if needed: VPS log rotation tutorial.

Step 5: SELinux notes (AlmaLinux/Rocky/RHEL family)

On SELinux-enforcing systems, chrooted SFTP can fail even when Unix permissions look perfect.

Start with the logs. Then work from evidence, not guesses.

getenforce
journalctl -u sshd --since "10 min ago" --no-pager
ausearch -m avc -ts recent | tail -n 20

Two common fixes:

  • Keep SFTP jail directories under a location with appropriate contexts.
  • Apply the correct SELinux labels if you bind-mount web roots into the jail.

If you’re not sure what you’re changing, don’t “solve” this by disabling SELinux. Fix the context, or place the jail under a standard path you can label consistently.

Step 6: Verify the jail is actually safe (quick test plan)

Don’t trust a config you haven’t tested. Run a quick, repeatable check.

  1. Login as the SFTP user and try to list / (it should show jail root contents only).
  2. Try uploading to the jail root (/) (should fail).
  3. Upload to upload/ (should succeed).
  4. Try SSH shell: ssh client1@server (should drop into SFTP/deny shell).
  5. Try port forwarding: ssh -L 8080:127.0.0.1:80 client1@server (should be blocked).

Operational extras: logging, auditing, and client handoff

SFTP issues are usually mundane: the wrong key, permissions drift, or an IP blocked by a firewall rule.

A clean handoff and a fast way to read logs saves time.

Find SFTP authentication failures fast

Ubuntu/Debian typically logs to:

/var/log/auth.log

RHEL-family systems often use:

/var/log/secure

Useful filters:

grep -E "sshd\[|sftp" /var/log/auth.log | tail -n 50
journalctl -u ssh --since "1 hour ago" --no-pager

Client handoff checklist

  • Server hostname or IP
  • Port (default 22 unless you changed it)
  • Username
  • Auth method: SSH key (preferred)
  • Remote path to upload (example: /upload or /site1)
  • What not to upload (large DB dumps, zip bombs, random backups) unless agreed

If you want to move SSH off port 22, plan the change. Otherwise, you can break deploy scripts and monitoring.

HostMyCode’s walkthrough is here: SSH port change tutorial.

Troubleshooting common SFTP jail failures

These are the same problems you’ll see over and over in real hosting environments.

“Connection closed” immediately after login

  • Chroot directory is writable by the user (must be root-owned and not group-writable).
  • Wrong permissions on /sftp or /sftp/username.
  • SELinux denial on AlmaLinux/Rocky.

Quick fix pattern:

chown root:root /sftp /sftp/client1
chmod 755 /sftp /sftp/client1

“Permission denied (publickey)”

  • Bad key pasted into authorized_keys (wrapped lines, missing prefix).
  • Wrong ownership/permissions on ~/.ssh or authorized_keys.
  • User is matched by a restrictive Match block unexpectedly.

Confirm permissions:

namei -l /home/client1/.ssh/authorized_keys

User can write everywhere inside the jail

This almost always means the chroot root was made writable during debugging.

Lock it back down and keep writes confined to a dedicated subfolder.

Uploads are slow or time out

Start with disk I/O wait and filesystem pressure. “SFTP is slow” is often storage contention, not a network problem.

Run:

uptime
iostat -xz 1 5 || apt-get -y install sysstat || dnf -y install sysstat
df -h

If iowait is high, work through this: VPS disk I/O troubleshooting tutorial.

Security checklist for SFTP on a hosting VPS

  • Keys only for SFTP users (PasswordAuthentication no in Match block)
  • SFTP-only command (ForceCommand internal-sftp)
  • No forwarding (AllowTcpForwarding no, PermitTunnel no)
  • Root-owned chroot root (and not writable)
  • Writable directory isolated (upload/)
  • Firewall allows SSH only from expected IP ranges when possible
  • Backups cover uploaded assets if they’re business-critical

Summary: a safer way to handle client uploads

SFTP-only accounts with chroot jails deliver a lot of security value for the effort involved.

Clients still get the workflow they expect—drag-and-drop transfers, automated deploys, or scheduled uploads—while you keep the server contained.

Add key-based auth and a few sane limits. You’ll spend less time cleaning up incidents and chasing “mystery permission” tickets.

If you want this on infrastructure that scales cleanly and recovers quickly, start with a HostMyCode VPS.

If you’d rather not manage SSH policy, permissions, and auditing yourself, HostMyCode’s managed VPS hosting covers the same practical needs: safe access, stable performance, and controlled change windows.

Need secure client uploads without handing out shell access? Use a HostMyCode VPS to control SSH policy and per-user SFTP jails, or choose managed VPS hosting if you want help maintaining hardened configs and scheduling safer changes.

FAQ

Can I use SFTP without giving users SSH shell access?

Yes. Use ForceCommand internal-sftp and set the user shell to /usr/sbin/nologin (or /bin/false).

Test that ssh user@host can’t open a shell.

Why must the chroot directory be owned by root?

OpenSSH enforces this to prevent users from modifying the jail environment.

If the chroot root is writable, a user could alter files that affect the session and potentially escape confinement.

Should I enable password logins for SFTP users?

For hosting operations in 2026, keys are the safer default.

If a client insists on passwords, limit by source IP and enforce strong password policy. Expect more compromise risk and more lockouts.

How do I give one SFTP user access to two different sites?

Create two bind mounts inside their jail (for example, /sftp/client1/siteA and /sftp/client1/siteB).

Keep the chroot root locked down. Only make the bind-mounted targets writable if you trust the user to edit live code.

What’s the safest way to rotate access for a contractor?

Use a dedicated user per contractor and a dedicated SSH key per device. When the contract ends, remove their public key (or lock the account). This avoids disrupting other users.