SFTP Server Setup with Chroot on Linux

When someone asks for “just FTP access” to a Linux server, I almost always translate that into “they need file transfer, but we should not open an actual FTP-shaped problem.” In most modern environments, SFTP is the right answer. It uses SSH, it is encrypted by default, it does not need a separate daemon in the common OpenSSH case, and it is much easier to harden than classic FTP.

The part that usually trips people up is not SFTP itself. It is safe containment. If a user should only upload files into a limited area, the chroot rules matter, the directory ownership rules matter, and the SSH Match block has to be precise.

This guide is how I set up a practical SFTP-only environment with OpenSSH, chroot jails, dedicated groups, logging, key authentication, and a few notes on running different policies for different user groups.

If you are hardening SSH generally, read the SSH hardening guide alongside this one. If you need a refresher on creating and managing accounts cleanly, the Linux user and group management guide is also directly relevant.

Why SFTP wins over FTP and FTPS most of the time

The naming is confusing because FTP, FTPS, and SFTP sound related. Operationally, they are very different.

  • FTP: old, plaintext by default, awkward with firewalls, not something I deploy on the public internet willingly
  • FTPS: FTP plus TLS, better than plain FTP but still inherits a lot of FTP complexity
  • SFTP: file transfer subsystem built on SSH, separate protocol, simpler to secure and audit in Linux environments

If I already trust OpenSSH on the server, SFTP is usually the least painful path.

One practical advantage people underestimate

Because SFTP rides on SSH, user management and key management stay inside a toolchain admins already know. I do not need a separate FTP daemon, separate auth backend, or a stack of passive port rules unless I have a very specific legacy requirement.

OpenSSH already includes what you need

On most Linux distributions, you do not need to install an extra SFTP package. The built-in OpenSSH subsystem is enough.

Confirm the server is installed and enabled:

sudo systemctl status ssh --no-pager

Or on distributions that use the sshd unit name:

sudo systemctl status sshd --no-pager

Check the current subsystem line:

grep '^Subsystem' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null

For a typical built-in setup, I want to see:

Subsystem sftp internal-sftp

If an older config points to an external sftp-server binary, I usually move to internal-sftp unless I have a reason not to. It is simpler and integrates nicely with chroot and forced-command controls.

Create a dedicated SFTP group

I do not like mixing normal shell users and SFTP-only users without a clear policy boundary.

Create a group:

sudo groupadd sftpusers

Then create a user and add them to the group:

sudo useradd -g sftpusers -s /usr/sbin/nologin -m upload1
sudo passwd upload1

I normally set the shell to /usr/sbin/nologin or /bin/false for SFTP-only users so even if a Match block is removed by mistake later, they do not get a useful interactive shell by default.

If the path differs on your distro, confirm it first:

command -v nologin

Understand the chroot ownership rule before you build directories

This is the rule that causes the most frustration:

the chroot directory itself must be owned by root and must not be writable by the user.

If that rule is violated, OpenSSH refuses the session with an error like:

bad ownership or modes for chroot directory

So I do not make the chroot root directory user-writable.

Instead, I create a root-owned chroot path and then a writable subdirectory inside it.

Build the directory structure correctly

A common pattern looks like this:

sudo mkdir -p /sftp/upload1/uploads
sudo chown root:root /sftp
sudo chmod 755 /sftp
sudo chown root:root /sftp/upload1
sudo chmod 755 /sftp/upload1
sudo chown upload1:sftpusers /sftp/upload1/uploads
sudo chmod 750 /sftp/upload1/uploads

In this setup:

  • /sftp/upload1 is the chroot root
  • it is owned by root:root
  • the user uploads into /uploads, which is inside the jail and user-writable

That separation is non-negotiable if you want a reliable chroot setup.

Why I prefer a separate top-level /sftp path

I can chroot users into /home/%u, but I usually prefer a dedicated tree like /sftp/%u because it keeps the policy obvious. Home directories often accumulate assumptions from other tools. A dedicated hierarchy keeps SFTP-specific ownership and permissions easier to reason about.

Configure sshd for SFTP-only access

Now the important part: the SSH daemon policy.

A minimal practical block looks like this:

Subsystem sftp internal-sftp

Match Group sftpusers
    ChrootDirectory /sftp/%u
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no
    PasswordAuthentication yes
    PubkeyAuthentication yes

A few notes:

  • ChrootDirectory /sftp/%u maps each user into their own directory under /sftp
  • ForceCommand internal-sftp prevents shell access and forces the SFTP subsystem
  • forwarding and X11 are disabled because SFTP users do not need them

Passwords or keys?

I support both if the use case requires it, but for production automation or regular partners I strongly prefer keys.

If you want keys only:

Match Group sftpusers
    ChrootDirectory /sftp/%u
    ForceCommand internal-sftp
    AllowTcpForwarding no
    X11Forwarding no
    PasswordAuthentication no
    PubkeyAuthentication yes

That is a much cleaner posture for repeatable access.

Validate the config before reload

Always:

sudo sshd -t

Then reload:

sudo systemctl reload sshd

Or on Debian-family systems:

sudo systemctl reload ssh

Keep an existing session open until you confirm the new behavior with a fresh login.

Test with the sftp client

From another system or a new terminal:

sftp upload1@example.com

If you changed the SSH port:

sftp -P 2222 upload1@example.com

Basic session checks I run immediately:

pwd
ls
cd uploads
put testfile.txt
get testfile.txt

If pwd shows /, that is normal inside the chroot. The user is jailed. Their real filesystem path is hidden from the session view.

Test with WinSCP too if Windows users are involved

If the actual users are on Windows, I validate with WinSCP before declaring success. A perfectly correct OpenSSH setup can still fail operationally if the user’s GUI workflow expects something different, like a writable landing directory or key format conversion.

Logging SFTP activity separately

When I am supporting partners, vendors, or semi-trusted upload users, I usually want better SFTP logging than the default generic SSH noise.

You can increase SFTP logging directly in the forced command:

Match Group sftpusers
    ChrootDirectory /sftp/%u
    ForceCommand internal-sftp -f AUTHPRIV -l INFO
    AllowTcpForwarding no
    X11Forwarding no

Then route those logs with rsyslog if desired. Example /etc/rsyslog.d/30-sftp.conf:

if ($programname == 'internal-sftp') then /var/log/sftp.log
& stop

Restart rsyslog:

sudo systemctl restart rsyslog

And test logging by transferring a file, then checking:

sudo tail -f /var/log/sftp.log

I like separate logs because they make troubleshooting file transfer issues much easier than digging through a general auth log stream.

Key-based auth for SFTP users

SFTP users can use normal SSH public keys, with one important operational wrinkle: the chroot directory ownership rules still apply.

A safe pattern is to keep the authorized keys in the user’s normal home metadata if your layout allows it, or define a central root-owned key directory.

One simple option is a central authorized keys directory:

sudo mkdir -p /etc/ssh/authorized_keys
sudo chmod 755 /etc/ssh/authorized_keys
sudo touch /etc/ssh/authorized_keys/upload1
sudo chmod 600 /etc/ssh/authorized_keys/upload1
sudo chown root:root /etc/ssh/authorized_keys/upload1

Then set in sshd_config:

AuthorizedKeysFile /etc/ssh/authorized_keys/%u

Add the user’s public key:

sudo tee -a /etc/ssh/authorized_keys/upload1 > /dev/null <<'EOF'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleReplaceThisWithARealPublicKey upload1@example
EOF

This root-owned central model avoids permission headaches inside tight chroot layouts.

If you need help generating or validating keys, the SSH key authentication guide covers that workflow in detail.

Set sensible ownership and collaboration rules inside the jail

Once the chroot root is correct, the next practical question is how users will actually work inside it.

For a single-user upload area, one writable directory is enough:

sudo mkdir -p /sftp/upload1/uploads/incoming
sudo chown upload1:sftpusers /sftp/upload1/uploads
sudo chmod 750 /sftp/upload1/uploads
sudo chown upload1:sftpusers /sftp/upload1/uploads/incoming
sudo chmod 750 /sftp/upload1/uploads/incoming

For shared drop zones, I usually create a dedicated group and use the setgid bit so new files inherit the group cleanly:

sudo groupadd partnerdrop
sudo usermod -aG partnerdrop upload1
sudo mkdir -p /sftp/upload1/uploads/shared
sudo chown root:partnerdrop /sftp/upload1/uploads/shared
sudo chmod 2770 /sftp/upload1/uploads/shared

That is much cleaner than trying to fix group ownership after every upload with cron.

Think about file overwrite and delete behavior

If multiple parties use the same directory, write permission alone may not be enough policy. In some cases I split areas into:

  • incoming/ for uploads
  • processed/ for files moved by backend jobs
  • archive/ for retained copies

That structure makes troubleshooting and automation easier, especially if another process picks up files from the SFTP jail later.

Permissions troubleshooting workflow I actually use

When an SFTP user says “login works but I cannot upload,” I do not guess. I check the path component by component.

First:

namei -l /sftp/upload1/uploads

Then I confirm the effective user and groups:

id upload1

Then I test the effective SSH policy:

sudo sshd -T -C user=upload1,host=example.com,addr=198.51.100.25 | grep -E 'chrootdirectory|forcecommand|passwordauthentication|pubkeyauthentication'

That three-command sequence catches most mistakes quickly.

On SELinux-enabled systems, I also check whether the policy layer is blocking writes even when Unix permissions look correct. If the logs hint at AVC denials, ausearch -m AVC -ts recent and restorecon -Rv /sftp are usually the next commands I run before I blame OpenSSH.

Separate automation accounts from human upload accounts

I have learned not to lump them together.

Automation accounts usually want:

  • key-based auth only
  • no password expiry drama
  • predictable upload path
  • dedicated ownership for downstream processing

Human users usually want:

  • clear landing directory names
  • simple client setup instructions
  • visible error feedback in WinSCP or another GUI
  • a supportable password or MFA policy

I often create different groups and different Match blocks for those two populations even when both use SFTP. The config is a little longer, but the behavior is much easier to reason about.

Restrict SFTP users to file transfer only

The key line is already doing most of the work:

ForceCommand internal-sftp

This denies shell access even if the user tries a normal SSH client.

Test it:

ssh upload1@example.com

A properly restricted user should not get an interactive shell. That is an important validation step.

I also keep forwarding disabled in the same match block because otherwise an SFTP-only account may still be able to use SSH features you did not intend to expose.

Multiple SFTP policies for different groups

This is where OpenSSH’s Match blocks are very useful.

Example:

Subsystem sftp internal-sftp

Match Group sftpusers
    ChrootDirectory /sftp/%u
    ForceCommand internal-sftp -f AUTHPRIV -l INFO
    AllowTcpForwarding no
    X11Forwarding no
    PasswordAuthentication no
    PubkeyAuthentication yes

Match Group vendorsftp
    ChrootDirectory /partner-data/%u
    ForceCommand internal-sftp -f AUTHPRIV -l VERBOSE
    AllowTcpForwarding no
    X11Forwarding no
    PasswordAuthentication yes
    PubkeyAuthentication yes

I use patterns like this when internal automation uses keys only but external vendors still require carefully managed passwords during a transition period.

The main thing is to document the order and intent. Complex Match logic gets forgotten faster than people think.

Bandwidth throttling: OpenSSH does not do this natively

This comes up regularly, and the answer disappoints people.

OpenSSH’s built-in SFTP does not provide per-user bandwidth throttling the way some dedicated file transfer platforms do.

Workarounds include:

  • traffic shaping with tc
  • shaping at the firewall or network edge
  • using a different service specifically built for managed transfer policies

I do not try to fake bandwidth control inside OpenSSH itself because it does not exist there.

Common errors and what they usually mean

“bad ownership or modes for chroot directory”

This almost always means the chroot root path is user-owned or user-writable.

Check with:

namei -l /sftp/upload1/uploads

That command is excellent because it shows ownership and permissions for every path component.

User logs in but cannot upload

Usually one of these:

  • writable subdirectory missing
  • wrong ownership on /uploads
  • permissions too restrictive
  • uploaded path not inside the chroot as expected

User gets a shell instead of SFTP

Usually one of these:

  • ForceCommand internal-sftp missing or not matched
  • user not in the expected group
  • conflicting Match blocks

To test effective config for a user and source address:

sudo sshd -T -C user=upload1,host=example.com,addr=198.51.100.25

That is one of my favorite OpenSSH troubleshooting commands because it shows the effective final settings after match processing.

Login fails after moving AuthorizedKeysFile

Usually a path or permission issue. If you use a central authorized keys directory, keep it root-owned and confirm the path exactly matches the AuthorizedKeysFile directive.

Security considerations I do not skip

  • use a dedicated group for SFTP-only accounts
  • force internal-sftp
  • disable shell access and forwarding
  • prefer key auth over passwords when possible
  • log transfers separately if the service matters operationally
  • keep the chroot root owned by root
  • review accounts periodically

If the service is internet-facing, I also consider Fail2ban for SSH noise reduction, just as I do for general SSH access. The Fail2ban article applies here too because SFTP rides on SSH.

Final thoughts

SFTP with OpenSSH is one of those setups that works very well once you respect its rules. The biggest rule is also the one people fight most: the chroot directory itself must be owned by root and not writable by the user. Once you accept that and build a writable subdirectory inside the jail, the rest becomes much more predictable.

My standard pattern is simple. Use internal-sftp. Put SFTP-only users in a dedicated group. Chroot them into a root-owned path like /sftp/%u. Give them a writable uploads directory inside it. Force the SFTP command. Disable forwarding. Prefer key-based auth. And test both sftp and plain ssh so you know the restriction really works.

That is not glamorous, but it is reliable, which is exactly what you want from a file transfer service.

Scroll to Top