How to give a client or developer SFTP access to one website only

Someone needs to upload files to one of your sites. Give them a login that can only move files, only in that site's folder. No shell, no other sites, and easy to remove later.

Keep your own SSH session open while you change the SSH settings, and test in a second window. If something goes wrong, you can still fix it.

Why SFTP, not FTP

Plain FTP sends the password and the files unencrypted, and it needs extra ports open. SFTP runs over SSH, which your server already has on port 22. Everything is encrypted, and every common program can use it: FileZilla, WinSCP, Cyberduck and the code editors with a "remote files" option. So you don't need to install an FTP server at all.

What you build here:

  • A user that can only use SFTP. Trying to open a shell fails.
  • The user is locked in a folder (a "chroot"). It sees the site's files and nothing else on the server.
  • The files it uploads belong to the site's group, so PHP can read them and the site keeps working.

One thing to know first: anyone who can upload PHP files to a site can run code as that site. Only give this to people you trust with the site.

Before you start

  • The site runs as its own user with its own PHP-FPM pool, as in our guide to one user and pool per site. The examples use that guide's names: the site user shop, the folder /var/www/shop.example and the website files in public.
  • The person getting access is called anna. Use your own names.
  • You log in with your own user that has sudo, not root. See use a sudo user instead of root.
  • Tested on Ubuntu 24.04 (OpenSSH 9.6) and Debian 13 (OpenSSH 10.0).

1. Make the user

sudo useradd --no-create-home --home-dir /srv/sftp/shop --shell /usr/sbin/nologin --groups shop anna

Anna can't log in to a shell, and she joins the shop group. That group is how she gets to the site's files. Check with id anna: the list of groups should include shop.

2. Make the folder she is locked in

SSH has one strict rule here: the lock folder, and every folder above it, must belong to root and must not be writable by anyone else. If not, SSH refuses the login (more on that error below). So the site folder itself can't be the lock folder, because it belongs to shop.

The usual way round it: make a small root-owned folder, and show the site's files inside it with a "bind mount". That makes the same files appear in a second place. Nothing is copied.

sudo mkdir -p /srv/sftp/shop/site
sudo chown root:root /srv/sftp /srv/sftp/shop
sudo chmod 755 /srv/sftp /srv/sftp/shop

Add the bind mount to /etc/fstab, so it comes back after a restart, then mount it now:

echo '/var/www/shop.example/public /srv/sftp/shop/site none bind 0 0' | sudo tee -a /etc/fstab
sudo systemctl daemon-reload
sudo mount /srv/sftp/shop/site

ls /srv/sftp/shop/site should now list the site's files. Anna will see a folder called site, and nothing else from the server. Only public is shown, so the site's sessions and tmp folders stay out of reach.

3. Let her change the site's files

Anna writes through the shop group, so the folders and files need group write rights. Two commands:

sudo find /var/www/shop.example/public -type d -exec chmod g+ws {} +
sudo find /var/www/shop.example/public -type f -perm -g=r -exec chmod g+w {} +
  • The first lets the group write in every folder. The s (called "setgid") makes new files and folders take the folder's group, shop, not Anna's own group. That's what keeps them readable by PHP.
  • The second adds write rights only to files the group can already read. Files you locked to the owner, such as WordPress's wp-config.php at 600, stay locked, so Anna can't read the database password. If she does need that file, change it on purpose.

PHP-FPM still runs as shop and still owns the site's files. Nothing changes for the site itself.

4. Add her SSH key

Ask Anna for her public key: the file ending in .pub. If she has none, she runs ssh-keygen -t ed25519 on her computer (see our SSH keys guide). Keep her keys in a root-owned place outside her folder, so she can't change them:

sudo mkdir -p /etc/ssh/authorized_keys
sudo nano /etc/ssh/authorized_keys/anna

Paste her key as one line and save. Then:

sudo chmod 755 /etc/ssh/authorized_keys
sudo chmod 644 /etc/ssh/authorized_keys/anna

5. Tell SSH to lock her in

sudo nano /etc/ssh/sshd_config.d/50-sftp-shop.conf
Match User anna
    ChrootDirectory /srv/sftp/shop
    ForceCommand internal-sftp -u 0002 -d /site
    AuthorizedKeysFile /etc/ssh/authorized_keys/%u
    DisableForwarding yes
    PermitTunnel no
    PermitTTY no

What each line does:

  • Match User anna: the lines below apply to Anna only. Your own login doesn't change.
  • ChrootDirectory: the folder she is locked in. To her, it looks like the whole server.
  • ForceCommand internal-sftp: whatever she asks for, she only gets SFTP. -u 0002 makes new folders group-writable, and -d /site starts her in the site's files.
  • AuthorizedKeysFile: read her key from the file you made in step 4.
  • DisableForwarding, PermitTunnel no: she can't use the connection to reach other services on the server, such as the database or another site.
  • PermitTTY no: no terminal.

Check the file, see what SSH will use for Anna, and apply it:

sudo sshd -t
sudo sshd -T -C user=anna,host=x,addr=203.0.113.5 | grep -Ei 'chroot|forcecommand|disableforwarding'
sudo systemctl reload ssh

sshd -t prints nothing when the file is fine. The second command should show your folder and internal-sftp. Run it with user= your own name too: there it should say chrootdirectory none. Reloading doesn't drop your open session.

Several people, or several sites?

For a few people on the same site, list them: Match User anna,ben. Or make a group, for example sftp-shop, add each person to it, and use Match Group sftp-shop. Each site gets its own folder in /srv/sftp, its own bind mount and its own Match block.

6. Only if she can't use a key: a password

Keys are safer. If Anna's program can't use one, turn passwords on for her only. Add this line at the end of the Match block from step 5:

    PasswordAuthentication yes

Then set a long, random password and apply:

sudo passwd anna
sudo sshd -t && sudo systemctl reload ssh

Because the line sits inside Match User anna, password login stays off for everyone else, including after you turned it off. Check: sudo sshd -T -C user=root,host=x,addr=203.0.113.5 | grep passwordauth should still say no.

An account with a password will get guessing attempts, so also set up Fail2ban. Its normal SSH rule covers SFTP too: in our test, five wrong SFTP passwords got the address blocked.

7. Test it

From a computer with Anna's key (or ask her to do it), connect:

sftp anna@your-server

Try a few things:

sftp> pwd
Remote working directory: /site
sftp> put index.php
sftp> mkdir assets
sftp> cd /
sftp> ls
site
sftp> cd /var/www
realpath /var/www: No such file
sftp> get /etc/passwd
File "/etc/passwd" not found.

She can upload and make folders, but outside site there is nothing: not your other sites, and not the rest of the server. Now check that a shell is refused:

ssh anna@your-server
This service allows sftp connections only.

On the server, check the new files. They belong to anna, with the group shop:

ls -l /var/www/shop.example/public

Open the site in a browser to see that PHP runs the uploaded files. In our tests, uploaded PHP files ran as shop, and files Anna overwrote kept their owner shop.

Good to know

  • New files are readable, but not always writable, by the site. Most programs upload with the rights the file had on Anna's computer, often 644. PHP can read that, but the site can't change the file. Usually that's what you want. If the site must change it, Anna can run chmod 664 file in SFTP, or you run the two commands from step 3 again.
  • Files the site makes, such as WordPress uploads or a cache, belong to shop and are often only readable by the group. If Anna needs to change them, run the step 3 commands again.
  • If the site deploys from Git, changes should go through the repository instead. Uploaded files get overwritten on the next deploy.
  • If your SSH logins ask for a code (see our two-step SSH guide), add AuthenticationMethods publickey to her Match block, or she can't log in.
  • On Ubuntu, a small .cache folder can appear next to site after her first login. It's harmless.

Remove access later

When the work is done, remove the login. First the SSH settings and the key:

sudo rm /etc/ssh/sshd_config.d/50-sftp-shop.conf /etc/ssh/authorized_keys/anna
sudo sshd -t && sudo systemctl reload ssh

Connections that are already open stay open after a reload. End them:

sudo pkill -u anna

Give her files back to the site user, so they don't end up with an unknown owner:

sudo find /var/www/shop.example -user anna -exec chown shop {} +

Remove the bind mount and the folder. rmdir only removes an empty folder, so it stops you if the site's files are still mounted there:

sudo umount /srv/sftp/shop/site
sudo sed -i '\#/srv/sftp/shop/site#d' /etc/fstab
sudo rmdir /srv/sftp/shop/site
sudo rm -r /srv/sftp/shop
sudo userdel anna

If you only want to pause access, remove the key file (or lock the password with sudo passwd -l anna) and end open sessions. Everything else can stay.

If something goes wrong

  • The connection closes right away, often with "Connection closed" or "Broken pipe". Look at the SSH log: sudo journalctl -u ssh -n 20. If it says bad ownership or modes for chroot directory, the lock folder or a folder above it isn't owned by root, or is group- or world-writable. Fix it with the chown and chmod 755 from step 2. Never make the lock folder writable for Anna: use the site folder inside it.
  • "Permission denied (publickey)": the key file is wrong, or not readable. Check that /etc/ssh/authorized_keys/anna holds her .pub key on one line, and that the file is 644 and owned by root.
  • She logs in but sees an empty site folder: the bind mount isn't there, for example after a restart with a typo in /etc/fstab. Run findmnt /srv/sftp/shop/site and sudo mount -a.
  • "Permission denied" when she uploads: the folder or file isn't group-writable, or she isn't in the shop group. Run the commands in step 3 and check id anna. A new group only counts from her next login.
  • The site shows an error for her new files: check their group with ls -l. It must be shop. If it isn't, the folder lacks the setgid bit from step 3.

Official sources

Next steps: if you haven't yet, turn off password login for everyone else, then work through the security checklist for a new PHP server.

ServerNest is in development. In our preview, each app gets its own SFTP login that uses an SSH key, stays inside the app's folder and can't open a shell. Join early access to try it first.

← Back to the security blog