1 SFTP
Drew edited this page 2026-05-02 01:52:01 -04:00

SFTP Server

Native OpenSSH internal-sftp, no extra daemon. SFTP-only users are chrooted to a shared bucket that Filedrop also writes into — meaning a web drag-drop and a desktop SFTP upload land in the same directory.

sshd configuration

Tail of /etc/ssh/sshd_config:

Subsystem	sftp	/usr/lib/openssh/sftp-server

Match Group sftpusers
    ChrootDirectory /var/sftp/shared
    ForceCommand internal-sftp
    PasswordAuthentication yes
    AllowTcpForwarding no
    X11Forwarding no

Reload after edits:

sudo sshd -t              # syntax check first — never restart on a broken config
sudo systemctl reload ssh

The permissions trick

This is what trips most people up when setting up sshd chroot. Two layers:

Path Owner Mode Why
/var/sftp root:root 755 parent of the chroot — must be root-owned
/var/sftp/shared root:root 755 the chroot itself — sshd refuses chroot if it's not root-owned and not group/world-writable
/var/sftp/shared/uploads nobody:sftpusers 777 the actual writable bucket, inside the chroot

The chroot dir itself is read-only to the SFTP users by design — they can cd and ls but can't drop files there. They can only write inside uploads/. This is the standard "chroot must be root-owned" requirement, sidestepped by giving them a writable sub-directory.

Users

User UID Primary group Shell Home
guest 1002 guest (1003) /sbin/nologin /var/sftp/shared
matt 1003 matt (1004) /sbin/nologin /var/sftp/shared
nate 1004 nate (1005) /sbin/nologin /var/sftp/shared

All three have sftpusers (gid 1001) as a supplementary group — that's what activates the Match block in sshd. Each user gets their own primary group, so file ownership is distinguishable inside uploads/.

/sbin/nologin ensures they cannot get an interactive shell even if password auth succeeds — only the sftp subsystem.

Adding a new SFTP user

sudo useradd -m -d /var/sftp/shared -s /sbin/nologin -G sftpusers <name>
sudo passwd <name>

-m -d /var/sftp/shared sets their home to the chroot — they land directly inside on connect. The supplementary group sftpusers triggers the Match block.

Removing a user

sudo userdel <name>          # leaves files owned by orphaned UID — fine for the shared bucket

Files in uploads/ are intentionally co-mingled, so there's no per-user home directory to clean up.

/opt/filedrop/docker-compose.yml mounts /var/sftp/shared/uploads into the tusd container as /uploads. tusd writes finalised uploads there directly — see Filedrop. This is what makes "web drop = SFTP drop" work.

Connecting

sftp guest@<host>
# or with a GUI: any SFTP client → host, port 22, password auth

SFTP users can be reached on:

  • LAN
  • Tailnet (discourse.<TAILNET>.ts.net)
  • Public IP — not exposed (no port 22 forwarding from router; only via Tailscale)