SFTP Users
Each SFTP user is a password-only login that is chrooted to one site's files. It gets no shell, TTY or forwarding. Files it uploads are owned by the site's user (wp_<slug>), so WordPress keeps working.
How it works
- System user:
useradd -M -o -u <site uid> -g <site gid> -G wphoster-sftp -d /srv/wphoster-sftp/<username> -s /usr/sbin/nologin. It shares the site user's UID/GID but is a separate account with its own password, and it can be locked on its own. - Jail:
/srv/wphoster-sftp/<username>(root:root 755). Itssite/is amount --bindof the site root. sshd refuses a chroot unless it and every parent are root-owned and not group/other-writable, which rules out the site root itself and anything under/var/www/wphoster. - sshd: one managed file,
/etc/ssh/sshd_config.d/zz-wphoster-sftp.conf, regenerated in full from the database. It contains:- Per-user IP restrictions first (
Match User u Address *,!ip…turns off every auth method). sshd takes the first value it sees, so these win. Match Group wphoster-sftp:ChrootDirectory /srv/wphoster-sftp/%u,ForceCommand internal-sftp -d /site -u 0022,PasswordAuthentication yes, and forwarding and TTY off. Password auth is enabled for this group only.Match allat the end, so nothing after theIncludeinsshd_configfalls inside the block.
sshd -t. If the check fails, the previous file is restored and the change is reported as failed. Only a valid config is reloaded. - Per-user IP restrictions first (
- Deactivating a user locks it (
usermod -L). Reactivating unlocks it. - Deleting a user unmounts
site/, runsuserdel(never-r), andrmdirs the jail. The site is never touched.
Flow
The panel never runs system commands in the web request. FtpUserController saves the row with status = pending and dispatches SyncSftpUserJob. The job applies the change and sets status to active, or to failed with last_error. The list page refreshes itself while anything is pending. The password travels to the job encrypted, and the database stores only a hash.
wphoster:sync-sftp runs every 5 minutes. It restores bind mounts after a reboot, removes SFTP users whose row or site is gone, and rewrites the sshd file if needed. It only re-provisions rows that are active. Pending and failed rows are left for the job or the admin.
Key files
app/Services/Sftp/SftpUserManager.php:provision(),deprovision(),buildSshdConfig(),writeSshdConfig(),syncAll(). All of them are idempotent.app/Jobs/SyncSftpUserJob.php,app/Console/Commands/SyncSftpUsers.phpapp/Http/Requests/Admin/FtpUsers/FtpUserRules.php: usernames are global system names. They must match^[a-z][a-z0-9_-]{2,31}$, can't start withwp_, can't be a site'sos_user, and can't be a reserved system name. Passwords need at least 12 characters.
Config
| Key | Purpose |
|---|---|
wphoster.paths.sshd_config_snippets | sshd include directory (/etc/ssh/sshd_config.d) |
wphoster.paths.sftp_jails_root | Jail root (/srv/wphoster-sftp) |
wphoster.sftp.group | Group every SFTP user belongs to (wphoster-sftp) |
wphoster.sftp.config_file | Managed sshd file name (zz-wphoster-sftp.conf) |
wphoster.sftp.ssh_service | systemd unit to reload (ssh) |
wphoster.sftp.host / .port | Connection details shown in the panel (defaults: server IP, 22) |
Prerequisites
The queue worker's user needs passwordless sudo for useradd, userdel, usermod, groupadd, chpasswd, mount, umount, mkdir, rmdir, chown, chmod, cp, mv, rm, cat, test, sshd -t and systemctl. The default sudoers in docs/DEPLOYMENT.md covers all of these. sshd_config must Include /etc/ssh/sshd_config.d/*.conf.