You were handed a VPS login, and now someone else needs to upload files to one folder on it. Sharing your own credentials is a bad idea. Here is the complete guide to creating an SFTP-only user who can add and edit files in a single folder, cannot open a terminal, and cannot see anything else on the server.
FTP vs FTPS vs SFTP: Which One to Use?
These three names look alike but are different technologies. For a live server, the choice is easy.
| Feature | FTP | FTPS | SFTP |
|---|---|---|---|
| Full name | File Transfer Protocol | FTP over SSL/TLS | SSH File Transfer Protocol |
| Encrypted | No (plain-text passwords) | Yes | Yes |
| Built on | Its own protocol | FTP + TLS certificates | SSH |
| Default port | 21 + random data ports | 21 / 990 + data ports | 22 (single port) |
| Extra server software | Yes (vsftpd, proftpd) | Yes, plus certificates | None, ships with OpenSSH |
| Firewall friendly | Poor | Poor | Good |
| SSH key login | No | Limited | Yes |
SFTP wins for this job: it runs through the SSH service already on your server, needs no new software or ports, and encrypts everything, including the password. OpenSSH can also lock a user into one folder and allow file transfer only.
sftp command.
How This Setup Works
SSH's ChrootDirectory option locks a user inside a folder so that folder looks like the server's root (/). It comes with one strict rule: the jail folder and every parent folder above it must be owned by root and must not be writable by anyone else.
A folder like <target_folder> sits inside a home directory owned by a normal user, so it cannot be a jail directly. The workaround is a root-owned jail folder with your real folder bind-mounted into it. A bind mount makes the same directory appear in two places at once.
Files uploaded over SFTP land directly in the real folder, and anything your own apps write there is visible to the SFTP user.
Prerequisites
- SSH access to the VPS with
sudorights. - OpenSSH server installed (it normally is). The commands below are for Ubuntu / Debian.
- The target folder already exists, and the owning user's group exists.
ssh -V
getent group <owner_group>
ls <target_folder>
Placeholders Used in This Guide
Every value that is specific to your server is written in angle brackets. Replace each one with your own value before running a command, and remove the angle brackets when you do.
| Placeholder | What it means | Example |
|---|---|---|
<username> | The restricted SFTP account you are creating | sftpuser |
<target_folder> | Full path of the existing folder you want to share | /home/appuser/shared |
<folder_name> | Name of that folder (what the user sees inside the jail) | shared |
<owner_group> | Group that owns the target folder (usually your app user's group) | appuser |
<server_ip> | Your VPS IP address or hostname | 203.0.113.10 |
<their_ip> | The SFTP user's static IP (optional hardening step) | 198.51.100.25 |
<second_username> | A second restricted user (maintenance section) | sftpuser2 |
sudo passwd <username> will fail if you paste it as-is, because the shell treats < and > as redirection. Swap in your real value first.
Step-by-Step Setup Walkthrough
Create a group for restricted users, then a user with no shell and no home directory of its own.
sudo groupadd sftpusers
sudo useradd -g sftpusers -G <owner_group> -s /usr/sbin/nologin -d /<folder_name> -M <username>
sudo passwd <username>
| Part | What it does |
|---|---|
groupadd sftpusers | Creates a group. SSH applies the restrictions to everyone in it, so adding more restricted users later is easy. |
-g sftpusers | Sets the user's primary group. |
-G <owner_group> | Adds the shared group (<owner_group>) so the user can write into the target folder. |
-s /usr/sbin/nologin | Gives the user no shell, so they can never get a terminal. |
-d /<folder_name> | Home directory as seen inside the jail. |
-M | Do not create a home directory (the jail provides it). |
passwd | Sets the password. Use a long, random one. |
sudo mkdir -p /srv/sftp/<username>/<folder_name>
sudo chown root:root /srv/sftp /srv/sftp/<username>
sudo chmod 755 /srv/sftp /srv/sftp/<username>
sudo mount --bind <target_folder> /srv/sftp/<username>/<folder_name>
mkdir -pcreates the jail folder with an empty<folder_name>folder inside it to act as the mount point.chownandchmodmake root the owner and block group/other writes, which SSH requires or it refuses the login.mount --bindmakes your real folder appear inside the jail.
A plain mount --bind disappears on reboot, so make it permanent through /etc/fstab:
echo '<target_folder> /srv/sftp/<username>/<folder_name> none bind 0 0' | sudo tee -a /etc/fstab
sudo mount -a
ls /srv/sftp/<username>/<folder_name>
mount -a re-reads fstab and reports any error immediately. The last command should list the same files and folders as your real target folder.
sudo chmod -R g+rwX <target_folder>
sudo find <target_folder> -type d -exec chmod g+s {} \;
g+rwXgives the<owner_group>group read/write on files and folders. The capitalXadds execute to folders only.g+s(setgid) on folders makes new files and subfolders inherit the<owner_group>group, so your own apps can still read and modify what the SFTP user adds.
Back up the config, then open it for editing:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo nano /etc/ssh/sshd_config
4a. Switch to the built-in SFTP server
Find the line that starts with Subsystem sftp and change it to:
Subsystem sftp internal-sftp
The default entry points to an external program that would not exist inside the jail. internal-sftp is built into the SSH daemon, so it works anywhere.
4b. Add the restriction block at the very end of the file
Match blocks must come last, because everything after a Match line applies only to that match.
Match Group sftpusers
ChrootDirectory /srv/sftp/%u
ForceCommand internal-sftp -u 0002
PasswordAuthentication yes
AllowTcpForwarding no
X11Forwarding no
AllowAgentForwarding no
PermitTunnel no
| Line | What it does |
|---|---|
Match Group sftpusers | Applies everything below only to members of sftpusers. |
ChrootDirectory /srv/sftp/%u | Locks the user into /srv/sftp/<username>. |
ForceCommand internal-sftp -u 0002 | Allows only SFTP, even if they try to run a command. -u 0002 makes uploaded files group-writable. |
PasswordAuthentication yes | Allows password login for this group even if it is disabled globally. |
The four No lines | Block port forwarding, X11, agent forwarding, and tunnels. |
4c. Validate and restart
sudo sshd -t
sudo systemctl restart ssh
No output from sshd -t means the config is valid. On RHEL / CentOS / Fedora the service is called sshd instead of ssh.
sftp <username>@<server_ip>
sftp> pwd # prints: Remote working directory: /
sftp> ls # shows only: <folder_name>
sftp> cd <folder_name>
sftp> ls # shows your existing files and folders
sftp> mkdir test-folder
sftp> cd /
sftp> cd /etc # must FAIL
sftp> exit
Then confirm that a normal SSH login is refused:
ssh <username>@<server_ip>
Expected result: "This service allows sftp connections only." Finally, check that test-folder exists in the real target folder, then remove it with rmdir <target_folder>/test-folder.
| Field | Value |
|---|---|
| Protocol | SFTP (not FTP) |
| Host | Your VPS IP |
| Port | 22 (or your custom SSH port) |
| Username | <username> |
| Password | The one set in Step 1 |
| Client | FileZilla, Cyberduck, or WinSCP |
Optional Hardening
Use SSH Keys Instead of a Password
Keys are far harder to guess or leak. The person generates a key pair on their computer and sends you only the public key:
ssh-keygen -t ed25519 -C "<username>"
The jailed user has no real home folder, so store the key in a root-owned location on the VPS:
sudo mkdir -p /etc/ssh/authorized_keys
sudo nano /etc/ssh/authorized_keys/<username>
sudo chown root:root /etc/ssh/authorized_keys /etc/ssh/authorized_keys/<username>
sudo chmod 755 /etc/ssh/authorized_keys
sudo chmod 644 /etc/ssh/authorized_keys/<username>
Paste the public key (one line) into the file, then update the Match block:
Match Group sftpusers
ChrootDirectory /srv/sftp/%u
ForceCommand internal-sftp -u 0002
AuthorizedKeysFile /etc/ssh/authorized_keys/%u
PasswordAuthentication no
AllowTcpForwarding no
X11Forwarding no
AllowAgentForwarding no
PermitTunnel no
sudo sshd -t && sudo systemctl restart ssh
sftp -i ~/.ssh/id_ed25519 <username>@<server_ip>
Always test key login before relying on PasswordAuthentication no.
Restrict by IP (If They Have a Static IP)
sudo ufw allow from <their_ip> to any port 22 proto tcp
Make sure your own IP is allowed on port 22 first, so you do not lock yourself out.
Block Brute-Force Attempts
sudo apt install fail2ban
The default configuration already protects SSH by temporarily banning IPs after repeated failed logins.
Back Up the Folder
tar -czf ~/backup-$(date +%F).tar.gz <target_folder>
Day-to-Day Maintenance
Change the Password or Disable the User
sudo passwd <username>
sudo usermod -L <username> # lock the account
sudo usermod -U <username> # unlock it again
If you use key authentication, locking the password may not block key login. Remove or rename the key file in /etc/ssh/authorized_keys/ to be sure.
Add Another Restricted User
Repeat Steps 1 to 3 with a new username. No change to sshd_config is needed because the Match Group sftpusers block already covers every member of the group.
sudo useradd -g sftpusers -G <owner_group> -s /usr/sbin/nologin -d /<folder_name> -M <second_username>
sudo passwd <second_username>
sudo mkdir -p /srv/sftp/<second_username>/<folder_name>
sudo chown root:root /srv/sftp/<second_username>
sudo chmod 755 /srv/sftp/<second_username>
echo '<target_folder> /srv/sftp/<second_username>/<folder_name> none bind 0 0' | sudo tee -a /etc/fstab
sudo mount -a
Check Logs and the Bind Mount
sudo tail -f /var/log/auth.log
findmnt /srv/sftp/<username>/<folder_name>
Removing the Setup Completely
/srv/sftp. Deleting files through a live bind mount deletes the real files in your target folder.
# 1. Unmount FIRST
sudo umount /srv/sftp/<username>/<folder_name>
# 2. Delete the <username> line from fstab
sudo nano /etc/fstab
# 3. Delete the user
sudo userdel <username>
# 4. Remove the now-empty jail folders (rmdir refuses non-empty folders)
sudo rmdir /srv/sftp/<username>/<folder_name>
sudo rmdir /srv/sftp/<username>
# 5. If you used key auth, remove the key file
sudo rm -f /etc/ssh/authorized_keys/<username>
# 6. Restore the original SSH config
sudo cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
sudo sshd -t && sudo systemctl restart ssh
Troubleshooting Common Errors
| Symptom | Likely cause | Fix |
|---|---|---|
bad ownership or modes for chroot directory |
Jail folder is not root-owned or is group/world-writable | Re-run the chown / chmod from Step 2 |
| Login works but the shared folder looks empty | Bind mount missing, or the folder was deleted and recreated | sudo umount /srv/sftp/<username>/<folder_name>; sudo mount -a |
| Permission denied when uploading | Group write permission missing | Re-run Step 3 and check id <username> lists <owner_group> |
| Connection closes right after login | Subsystem line or chroot path is wrong | Re-check Step 4a and the ChrootDirectory line |
| Permission denied (publickey) | Key file permissions wrong, or passwords disabled too early | Fix ownership/modes, or re-enable passwords while debugging |
sshd -t prints errors |
Typo in sshd_config |
Fix the named line or restore sshd_config.bak |
| Your apps can't edit uploaded files | Wrong group or umask | Confirm setgid on folders and -u 0002 in ForceCommand |
For anything else, watch the log live while attempting a login: sudo tail -f /var/log/auth.log.
Quick Command Summary
The whole setup in one block, for reference. Edit /etc/ssh/sshd_config as shown in Step 4 where indicated.
# Step 1: user
sudo groupadd sftpusers
sudo useradd -g sftpusers -G <owner_group> -s /usr/sbin/nologin -d /<folder_name> -M <username>
sudo passwd <username>
# Step 2: jail + bind mount
sudo mkdir -p /srv/sftp/<username>/<folder_name>
sudo chown root:root /srv/sftp /srv/sftp/<username>
sudo chmod 755 /srv/sftp /srv/sftp/<username>
sudo mount --bind <target_folder> /srv/sftp/<username>/<folder_name>
echo '<target_folder> /srv/sftp/<username>/<folder_name> none bind 0 0' | sudo tee -a /etc/fstab
sudo mount -a
# Step 3: permissions
sudo chmod -R g+rwX <target_folder>
sudo find <target_folder> -type d -exec chmod g+s {} \;
# Step 4: SSH config (edit sshd_config as described, then validate)
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo nano /etc/ssh/sshd_config
sudo sshd -t && sudo systemctl restart ssh
# Step 5: test
sftp <username>@<server_ip>

0 Comments