How to create new sftp user with access to /var/www

In what appears to be the new way I do things (i.e. here and here), following a question from TurnKey Hub user Rob, I'm posting this "tutorial" as a blog post.

Via Hub support, Rob asked about the best way to set up a user who can SFTP into his TurnKey WordPress server and upload/download files. He noted that he had followed my vague advice on a support thread (I'm not sure which one as he didn't explicitly say - perhaps one of these?). Regardless, it wasn't working as he'd hoped. I gave him some further limited, somewhat vague guidance, with an intention to look into it a little further and clarify details.

The more I dug, the clearer it became that there were a lot of intricacies and a lot of opinion about what is "best". Surely Rob is not the only one trying to do this?! So I decided to have a bit of a play with it and define a process that (hopefully) might be useful!?

The new SFTP user

In this "tutorial" post I'll document how to create an sftp user with write access to /var/www so that files can be uploaded/downloaded and modified. This will describe how to create a "locked down" user who can only access sftp, is "chroot jailed" within /var/www and can't log in via SSH at all.

If you're interested in configuring a limited SSH user who can log in, then I may cover that another day. If that interests you, please let me know (e.g. in the comments below).

Note that whilst I make every effort to document a secure setup, allowing any access to your server always has risks. The more access, the more risk. So whenever possible use the "principle of least privilege" and only give the user the permissions needed to do the required job and always only access to people you trust.

I've written these instructions specifically for TurnKey GNU/Linux v17.x (based on Debian 11/Bullseye), but they should work on any recent TurnKey system. It should also work on Debian or derivatives, e.g. Ubuntu. They may even work with other Linux systems, but there may be specific paths and commands that differ.

These instructions assume that you are running as root. If not, try prefixing commands with sudo, or better still, open a root terminal (e.g. 'sudo su -'). They also assume that the default webserver user group is 'www-data'.

Important note: Don't do this if you are hosting from /var/www

I consider it important that you DO NOT follow these instructions if your docroot is /var/www. These instructions require that /var/www is owned by root. Hosting from subdirectories is fine - otherwise move your docroot to a subdir first.

These instructions will make /var/www the HOME (i.e. '~') for this sftp user. So long as root maintains ownership of /var/www your sftp user won't be able to write files to /var/www so the risk of data leakage is minimal. However, if /var/www is owned by your new user (or www-data), ssh log in is allowed and you serve directly from /var/www (i.e. you don't follow these instructions), you risk leaking sensitive user info via your webserver; likely compromising the new user.

If you are using a TurnKey appliance that is based on LAMP; such as WordPress, Joomla, Drupal, Laravel, Nextcloud, or any of the other ones (about 70% of the library) you should be fine as they are hosted in a sub directory by default (e.g. /var/www/wordpress). Note that the base appliances (e.g. LAMP, LAPP, Ngnix, LigHTTPd) DO serve from /var/www docroot.

If you are hosting directly from /var/www then move your docroot before taking any of these steps. If you need a hand with that, please feel free to ask, be sure to share details of your setup.

Create the new user and add to 'www-data' group

Under normal circumstance, you would use 'adduser' to create a new user account. However, we have some specific requirements, so we're going to use the lower level 'useradd'.

Create the Linux user 'sftpuser' (change the name if you wish) with the specific requirements (use a better password):

useradd sftpuser --home-dir /var/www \
                 --gid www-data \
                 --shell /usr/sbin/nologin \
                 --password 'YourAwesomePassword'

Configure correct permissions

Assuming that the docroot is '/var/www/wordpress' (substitute your relevant docroot wherever you see '/var/www/wordpress' - e.g. '/var/www/drupal', '/var/www/joomla', etc) then "fix" permissions like this:

for dir in /var /var/www; do
    chown root:root $dir
    chmod 0755 $dir
chown -R www-data:www-data /var/www/wordpress

FYI permissions for '/var' & '/var/www' should probably be ok by default. But it won't work if they're wrong and resetting them to what they should be does no harm.

To ensure that our new user will have access to everything in /var/www, we'll need to ensure that the permissions allow group read & write. FYI the "execute permission" on directories is often called the "search bit" - and is required to view the directory contents. The below will also set the 'setgid' bit for directories which should make new files inherit the group ownership. TBH, I'm not 100% sure how (or if) that affects SFTP usage? Anyway, this should do the job:

find /var/www/wordpress -type d -exec chmod 2775 {} \;
find /var/www/wordpress -type f -exec chmod 0664 {} \;

Configure SSH/SFTP

We also need to ensure that ssh is configured to use it's internal sftp code. You can check that like this:

grep ^Subsystem /etc/ssh/sshd_config

If it's already configured, it should return output like this:

Subsystem   sftp    internal-sftp

If it looks like that, you're good to skip to the specific user ssh config just below. Otherwise, edit the file to make it look like above. Here's a command that will update it for you:

sed -i "\|^Subsystem\tsftp| s|/usr/lib/openssh/sftp-server|internal-sftp|" /etc/ssh/sshd_config

Now add the SSH config which will catch the 'sftpuser' and apply this specific config:

cat >> /etc/ssh/sshd_config.d/sftpuser.conf <<EOF
# SSH config for SFTP only user
Match User sftpuser
    ForceCommand internal-sftp -u 002
    ChrootDirectory /var/www/
    PasswordAuthentication yes
    X11Forwarding no
    AllowTcpForwarding no
    AllowAgentForwarding no
    PermitTunnel no

And now restart SSH:

systemctl restart ssh

That's it!

You should be able to download & upload files via SFTP using your new user. But not log in via SSH. A quick and dirty test can be done from the CLI:

touch test.html
sftp sftpuser@localhost

That should ask for your sftpuser password and drop you into the chroot ('/var/www' - which will report as '/' in the sftp session) with an sftp prompt:

sftpuser@localhost's password:
Connected to localhost.

Now you can 'put' the test file into the wordpress directory:

sftp> put test.html wordpress/
Uploading test.html to /wordpress/test.html


sftp> cd wordpress
sftp> put test.html
Uploading test.html to /wordpress/test.html

Then exit out and double check the test file:

ls -l /var/www/wordpress/test.html

It should return something similar to this:

-rw-r--r-- 1 sftpuser www-data 0 Oct 27 06:57 /var/www/wordpress/test.html

As you might have noticed (if you didn't follow my suggestion exactly), the default starting point will be /var/www, but the 'sftp' user will see that as '/' and won't have write permissions there (only in sub-directories). So as per my example above, either 'put' directly to the remote subdir, or cd to the desired remote subdir first.

Now you could also double check ssh login:

$ ssh sftpuser@

sftpuser@'s password:
This service allows sftp connections only.
Connection to closed.

A word on permissions

One thing that isn't as neat and tidy as I might like is the fact that the files won't always have write access by the webserver by default. The '-u 002' switch appended on the 'ForceCommand' directive sets the umask for the sftp user to '002' which should give group write permissions. But unfortunately, I hadn't considered that a mask is subtractive so it doesn't work as I had hoped. I'm not 100% sure how it might act when using an SFTP client from Windows, but when used from a Linux client, the existing permissions of the file(s) (before uploading to the server) are the starting point. So if the original file doesn't have group read/write access then the uploaded won't by default either.

So please be aware that you may hit issues if the webserver doesn't have write permissions to specific locations it may need (e.g. config.php needs to be writable to change config from the admin web UI).

The "fix" is to ensure that all files and directories have 'write' permissions. One way to do that is to rerun the find commands from above (as root):

find /var/www/wordpress -type d -exec chmod 2775 {} \;
find /var/www/wordpress -type f -exec chmod 0664 {} \;

Or if you know the exact files/directories then you can chmod them. Files should be 664 and directories 775 (or 2775). Files can be changed via an SFTP shell (or client) too if you want. If using sftp shell, then it's very similar to in normal shell:

chmod 644 path/to/file

Good luck

Hopefully you find this of value. If you find it helpful, or have other feedback and/or suggestions, please feel free to post below.


Very Siberian's picture

Thank you, Jeremy! This seems to be a common need on my TKL sites, so I really appreciate the detailed tutorial.

Best regards,

Rob (Very Siberian)

Mike Riley's picture

It would be much more helpful for my website project.

w0rdPr3ssFr13nd's picture

It works like a charm!
Leif's picture

I tried to get fancy and change the "drop in" directory at /var/www/tobaron/html/ because I am sure that's possible somehow but my atempt at that failed, I couldn't log in, so I backed out and just followed your instructions and it worked - so that's good enough - gets the job done.

BTW where you wrote


I think you meant sftp user@localhost; you're missing a space

Jeremy Davis's picture

IIRC if you make /var/www/tobaron/html/ the home directory of the user, then you should be able to get that to work.

Although I would NOT recommend using that path as it is likely being served. And if it's being served, then so are all the users files, unless you explicitly disable that in the webserver config. Whilst you could work around that via webserver config, I recommend making the dir one level up (i.e. /var/www/tobaron/) the user's home dir instead. Then they will still have access to the /var/www/tobaron/html/ directory, but limited risk of serving sensitive user files.

Also you'll need to ensure that the permissions are correct (owned by user, accessible by group). If you want to try again, please share a bit more info about the errors you hit and I'll do my best to help out.

Also, thanks for the heads up re the typo you found. You were totally on the money that it was wrong, although 'sftpuser' is the example username so it is missing the 'sftp' command completely. I.e. instead of:


It should be (and now is - I've updated/fixed it):

sftp sftpuser@localhost
Nono's picture

Hello, I tried your tutorial but it doesn't work on my side. I have wordpress folder at this location:
Therefore, my root folder for my websites should be /var/www/html. Here is the I run in order to do all the steps:
userdel sftp;

useradd sftp --home-dir /var/www/html \
                 --gid www-data \
                 --shell /usr/sbin/nologin \
                 --password 'test'
for dir in /var /var/www /var/www/html ; do
    chown root:root $dir
    chmod 0755 $dir

chown -R www-data:www-data /var/www/html/wordpress;

find /var/www/html/wordpress -type d -exec chmod 2775 {} \;
find /var/www/html/wordpress -type f -exec chmod 0664 {} \;

rm /etc/ssh/sshd_config.d/sftp.conf
cat >> /etc/ssh/sshd_config.d/sftp.conf <<EOF
# SSH config for SFTP only user
Match User sftp
    ForceCommand internal-sftp -u 002
    ChrootDirectory /var/www/html
    PasswordAuthentication yes
    X11Forwarding no
    AllowTcpForwarding no
    AllowAgentForwarding no
    PermitTunnel no
  And after:
systemctl restart ssh;

sftp sftp@localhost
When I try with password = test,   I have permission denied error. I have uncommented line "Subsystem   sftp    internal-sftp" in /etc/ssh/sshd_config.   Do you know why I got this error? Thank you in advance, Arno.
Jeremy Davis's picture

I'm not sure why it's not working for you? I will try to rerun it myself to double check that it still works, but I'm not sure when that will be.


Add new comment