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.
Cross-link with Filedrop
/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)
Navigation
Quick Links
Projects
Server (discourse)
- Host
- Cloudflare Tunnel
- Tailscale
- SFTP
- Discourse
- Forgejo
- Filedrop
- Filebrowser
- Landing Sites
- Onboarding
- Backups
- Recovery
Window Managers
Homelab
Docker
Software
Thoughts
Archived
Connect
Butterbian — my Debian 13 distro · butterrepo — community APT repo · butterknife — TUI installer
justaguylinux.com · Butterforge · Butter Lab · The Churn · YouTube · Mastodon · Links · Contact · Support on Buy Me a Coffee
"Not an expert. Just A Guy." — Licensed CC BY-SA 4.0 · © 2026 JustAGuy Linux