
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/%uexpects/sftp/client1,/sftp/client2, etc.PasswordAuthentication noinside 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
/sftplives (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.
- Login as the SFTP user and try to list
/(it should show jail root contents only). - Try uploading to the jail root (
/) (should fail). - Upload to
upload/(should succeed). - Try SSH shell:
ssh client1@server(should drop into SFTP/deny shell). - 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:
/uploador/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
/sftpor/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
~/.sshorauthorized_keys. - User is matched by a restrictive
Matchblock 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 noin 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.