Create the user
# A user with no shell, no home directory (the chroot replaces it)
sudo groupadd sftpusers
sudo useradd -g sftpusers -d /var/sftp/vendor-a -s /usr/sbin/nologin vendor-a
# Set a password (or use pubkey only; see below)
sudo passwd vendor-a
The chroot directory ownership rule
The unforgiving constraint: ChrootDirectory and every parent directory above it must be owned by root and not group/world writable. If the chroot is /var/sftp/vendor-a, then both /var/sftp and /var/sftp/vendor-a must be root:root with mode 755.
This means the user can't write to their own chroot root directly. The standard pattern: chroot is root-owned, and a sub-directory inside it (the user's actual writable space) is owned by them.
# Chroot directory hierarchy
sudo mkdir -p /var/sftp/vendor-a
sudo chown root:root /var/sftp /var/sftp/vendor-a
sudo chmod 755 /var/sftp /var/sftp/vendor-a
# Writable upload area for the user
sudo mkdir /var/sftp/vendor-a/uploads
sudo chown vendor-a:sftpusers /var/sftp/vendor-a/uploads
sudo chmod 755 /var/sftp/vendor-a/uploads
# Read-only download area
sudo mkdir /var/sftp/vendor-a/downloads
sudo chown root:sftpusers /var/sftp/vendor-a/downloads
sudo chmod 750 /var/sftp/vendor-a/downloads
sshd_config
Add to /etc/ssh/sshd_config (or a drop-in under /etc/ssh/sshd_config.d/):
# At the bottom of the file (Match blocks must come last)
Subsystem sftp internal-sftp
Match Group sftpusers
ChrootDirectory %h # %h = home dir = /var/sftp/vendor-a
ForceCommand internal-sftp
AllowTcpForwarding no
AllowAgentForwarding no
X11Forwarding no
PermitTTY no
PermitTunnel no
GatewayPorts no
Key directives:
Subsystem sftp internal-sftp— use the in-process SFTP server (no externalsftp-serverbinary in the chroot needed).ChrootDirectory %h— chroots to the user's home directory.ForceCommand internal-sftp— even if the user manages to request a shell, they get SFTP.AllowTcpForwarding no+ the rest — close off other SSH-feature escape hatches.
# Validate and reload
sudo sshd -t
sudo systemctl reload ssh
Test it
# From a client
sftp vendor-a@server.example.com
# Connecting to server.example.com...
# vendor-a@server.example.com's password: ...
# sftp> pwd
# Remote working directory: /
# sftp> ls
# downloads uploads
# sftp> cd uploads
# sftp> put localfile.txt
# Uploading localfile.txt to /uploads/localfile.txt
# sftp> cd /etc # Try to escape the chroot
# Couldn't canonicalize: No such file or directory <-- contained
# Try SSH command execution — refused
ssh vendor-a@server.example.com 'ls -la'
# This service allows sftp connections only.
Pubkey-only (recommended)
Passwords on file-transfer-only accounts are not great; pubkeys are better. The catch with chrooted users: ~/.ssh/authorized_keys inside the chroot must follow the same root-owned-parents rule.
Two paths:
- AuthorizedKeysFile outside the chroot — store keys in a system-managed location:
This is the cleanest pattern — admin-managed keys, no chroot-mount headache.# In sshd_config AuthorizedKeysFile /etc/ssh/authorized_keys/%u # Then per user sudo mkdir -p /etc/ssh/authorized_keys sudo chown root:root /etc/ssh/authorized_keys sudo chmod 755 /etc/ssh/authorized_keys echo "ssh-ed25519 AAAA... vendor-a-laptop" | \ sudo tee /etc/ssh/authorized_keys/vendor-a sudo chmod 644 /etc/ssh/authorized_keys/vendor-a sudo chown root:root /etc/ssh/authorized_keys/vendor-a - Conventional
~/.ssh/authorized_keysinside the chroot — create root-owned.sshsubdirectory and write the keys file (owned by the user); permission carefully:
Works but more fragile; (1) is preferred.sudo mkdir /var/sftp/vendor-a/.ssh sudo chown root:root /var/sftp/vendor-a/.ssh sudo chmod 755 /var/sftp/vendor-a/.ssh # But sshd inside the chroot is reading authorized_keys as the user # So the file inside must be owned by the user echo "ssh-ed25519 ..." > /tmp/keys sudo install -m 600 -o vendor-a -g sftpusers /tmp/keys /var/sftp/vendor-a/.ssh/authorized_keys
Once pubkey auth works, disable password auth for these users:
# In sshd_config under the Match Group block
PasswordAuthentication no
PubkeyAuthentication yes
Useful additions
Quotas: limit disk use per user
# Install quotas
sudo apt install quota
# Enable user quotas on the relevant filesystem in /etc/fstab:
# /dev/sdb1 /var/sftp ext4 defaults,usrquota 0 2
sudo mount -o remount /var/sftp
sudo quotacheck -cum /var/sftp
sudo quotaon /var/sftp
# Set per-user limit (1 GB soft, 1.5 GB hard)
sudo setquota -u vendor-a 1000000 1500000 0 0 /var/sftp
Rate-limit transfers
Use the -l option in internal-sftp (logs only); for bandwidth limits, wrap in tc or use scp's -l from the client.
fail2ban for failed logins
CrowdSec (see that tutorial) or fail2ban with the sshd jail catch brute-force on the SFTP-only account too — same as regular SSH.
One-shot upload accounts
For "this vendor delivers one file then never logs in again," combine with ChrootDirectory + a OnSession hook that disables the account after success:
# /usr/local/bin/disable-after-upload.sh
#!/bin/bash
# called from a pam_exec line; disable account after the SFTP session ends
sudo usermod -L "$PAM_USER"
More complex than needed for most use cases — usually a periodic cron rotation of keys is cleaner.
Worth knowing
- Logs. Set
LogLevel VERBOSEinsshd_config;journalctl -u ssh -fshows every SFTP file operation with timestamps + user. - Don't put the chroot on /home. The default user-skeleton, login scripts, and other surprises in /home parents make the "root-owned + 755" rule annoying.
/var/sftp/<user>is the canonical location. - Test with a regular SSH user first to confirm the server-side config doesn't break general SSH.
Match Group sftpusersonly affects the group; regular users keep their shells. - For 100+ third-party users, a dedicated SFTP appliance (Files.com, MFT solutions, or open-source like sftpgo) is more operational. For 1-10 users, plain OpenSSH is right.