为什么你的服务器还在用 scp 传文件
很多个人站长管理服务器的方式,从买第一台 VPS 那天起就没变过:本地开一个终端,敲一条 scp -r ./dist root@1.2.3.4:/var/www/,传完关掉窗口,一天的工作结束。这个流程在前两年没什么问题,直到站点开始变大——备份包动不动几个 GB,素材库几百个文件,日志导出一次几十万行。这时候 scp 的两个老毛病就会同时暴露出来:
第一个是断点不能续传。传到 87% 网络抖一下,或者本地笔记本合盖休眠,连接断了,游戏结束,你得整包重来。几百兆的静态资源包,可能一晚上就耗在这条断了的重传上。
第二个是没有目录树语义。scp 传目录靠的是本地递归展开再逐个上传,服务器上不存在的空目录它不会建,软链接会被解引用成实体文件,权限位也经常和本地不一致。等你发现线上 uploads/2026/09/ 这个空目录没建起来导致图片全 404 时,已经是第二天早上。
这篇文章换一条路:用 SSH 自带的 SFTP 子系统把服务器做成一个可挂载、可续传、可断点恢复的文件通道。不需要额外开端口,不需要装第三方服务,所有流量依旧走你那条已经加固过的 22 端口。
先把 sftp 子系统和用户隔离配好
OpenSSH 从 4.8 版起就内置了 SFTP 子系统,绝大多数发行版默认已经启用。先确认一下配置里这行还在:
grep -n "^Subsystem" /etc/ssh/sshd_config
# 期望输出:
# Subsystem sftp /usr/lib/openssh/sftp-server如果这条被注释掉了,SFTP 直接不可用,所有客户端连上来只会看到 Connection closed。补上之后记得 systemctl reload sshd。
更值得做的是用户隔离。个人站长常见做法是直接用 root 走 SFTP,这其实很不划算——一旦本地机器中毒、私钥被偷走,攻击者拿到的是整台服务器的 root 权限。正确姿势是专门开一个只能传文件、只能进自己目录的账号:
useradd -m -s /usr/sbin/nologin filesync
passwd filesync
mkdir -p /srv/filesync/upload
chown filesync:filesync /srv/filesync/upload然后在 /etc/ssh/sshd_config 末尾追加一个 Match 块:
Match User filesync
ChrootDirectory /srv/filesync
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
PermitTunnel no
PasswordAuthentication yes这里有三个特别容易踩的坑,我在自己的机器上全踩过一遍:
坑一:ChrootDirectory 以及它上面所有的父目录必须属于 root,且组和其他用户不可写。这是 sshd 的硬性要求,因为要防止被 chroot 的用户通过重命名目录逃逸。很多教程只教你 chown filesync:filesync /srv/filesync,结果连上去就是一句冷冰冰的 Connection closed by remote host,日志里写着 fatal: bad ownership or modes for chroot directory component。正确做法是让 /srv/filesync 归 root,只在里面开一个属于 filesync 的子目录给他写:
chown root:root /srv/filesync
chmod 755 /srv/filesync
chown filesync:filesync /srv/filesync/upload
# 验证一遍每一层
namei -l /srv/filesync/uploadnamei -l 会把路径上每一层的属主和权限列成一张表,哪一层不合格一眼就能看出来,比反复重启 sshd 试错快得多。
坑二:ForceCommand internal-sftp 和 ChrootDirectory 必须同时存在。只写 ForceCommand 不写 Chroot,用户还是能看到整个文件系统;只写 Chroot 不写 ForceCommand,用户 chroot 之后还能拿到一个 shell。两个一起写才既锁目录又锁命令。
坑三:chroot 之后内部路径会变。用户在 SFTP 客户端里看到的 / 实际对应服务器上的 /srv/filesync,所以别人给你的文档里写的绝对路径 /var/www/xxx 在他这边是访问不到的,得换算成 /upload/xxx。这点在写部署脚本时经常算错,值得先在客户端里手动 ls 一遍确认。
用 sshfs 把远程目录挂成本地磁盘
SFTP 客户端传输解决了断点续传,但每天还要敲命令终究麻烦。如果你用 macOS 或 Linux 做主力机,sshfs 能把远程目录直接挂载成本地路径,之后用编辑器、rsync、甚至图形化文件管理器操作它,都像在操作本地硬盘:
# Debian / Ubuntu
apt install sshfs
# macOS 需要先装 macFUSE,再装 sshfs
brew install --cask macfuse
brew install gromgit/fuse/sshfs-mac
# 挂载(注意不要用 root 账号,用刚才那个隔离账号)
mkdir -p ~/mnt/site
sshfs filesync@1.2.3.4:/upload ~/mnt/site -o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3那串选项是挂载能不能用得舒服的关键。reconnect 让 sshfs 在连接断开后自动重连,而不是让整个挂载点变成僵尸;ServerAliveInterval=15 和 ServerAliveCountMax=3 让客户端每 15 秒发一次心跳,连续三次没回应才判定断开。这两组参数一起用,能挡掉绝大多数「笔记本睡眠醒来后目录卡死、df 命令挂住不动」的情况。
不过 sshfs 有两个真实的取舍必须提前说清楚,否则容易在关键时刻掉链子:
第一,它是网络文件系统,不是本地磁盘。打开一个几万文件的大目录,sshfs 会逐个发 stat 请求,慢到你以为死机。所以它适合挂 uploads/ 这种小目录做日常拖拽,不适合挂整个 /var/www 再用 IDE 全量索引。更不要在上面跑数据库文件或者 git clone 一个大型仓库。
第二,网络断开的瞬间,任何正在读写该挂载点的进程都会变成不可中断状态。这时候 umount 会返回 target is busy,kill -9 也杀不掉那个进程。正确的抢救顺序是先 umount -l ~/mnt/site(lazy unmount,先解除命名空间引用,让新访问立刻失败),再用 ps aux | grep D 找到那个 D 状态进程,等它自己因 I/O 失败返回;实在不行再重启本地机器。这个坑和 NFS 卡死是同一类问题,只是很多人以为 sshfs 更轻量所以不会遇到。
自动化场景下用 sftp 批处理,别用 scp
如果在写部署脚本或定时备份,命令行里 sftp 支持批处理模式,比 scp 更适合无人值守场景。它最大的好处是失败会返回非零退出码并且能 get -a 续传:
#!/bin/bash
set -euo pipefail
# 用 -b 指定批处理文件,-oBatchMode=yes 禁止交互式提问(否则脚本会卡住)
sftp -b - -oBatchMode=yes -oServerAliveInterval=15 \
-i /root/.ssh/id_ed25519_backup \
filesync@1.2.3.4 <<'SFTP_CMDS'
cd /upload
lcd /var/backups
put -a site-2026-09-28.tar.gz
ls -l site-2026-09-28.tar.gz
bye
SFTP_CMDS
echo "上传完成,退出码 $?"几个细节值得抠一下。-oBatchMode=yes 非常关键,它让 sftp 在需要输入密码或确认主机指纹时直接失败退出而不是挂起等待——cron 里没有终端,一旦挂起这个任务就永远卡在那里,而且 crontab 不会通知你。put -a 里的 -a 是 append/resume 语义,同名文件已存在一部分时会从断点继续,这正是 scp 缺的那个能力。lcd 切的是本地目录,cd 切的是远程目录,两个命令别混。
如果你更习惯用 rsync 做增量同步,那也没问题,rsync 在 SSH 之上跑的是自己的协议,它天然支持断点续传和校验和比对,--partial --append-verify 这套组合是备份场景的标配。本文讲的 SFTP/sshfs 更适合「人工拖文件」和「异构脚本环境」这两种场景,两者并不冲突,按用途选就行。
安全收尾:把这条通道也纳入监控
新增一个可登录的账号,等于新增一个攻击面,收尾工作不能省。第一,给 filesync 也配上 SSH 密钥登录并关掉密码认证(把上面配置里的 PasswordAuthentication yes 改成 no),因为 SFTP 账号被暴力破解的案例一点不比 root 少。第二,在 fail2ban 里加上 sshd 的规则,让爆破尝试自动封禁:
# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
maxretry = 3
findtime = 600
bantime = 3600第三,也是最容易被忽略的一点:定期检查这个账号到底传了什么。chroot 只能限制他能去哪里,限制不了他传上来一个 PHP webshell。可以用 inotifywait 监听上传目录,一旦落进来 .php、.sh、.jsp 这类可执行文件就告警:
inotifywait -m -r -e create,moved_to /srv/filesync/upload \
--format '%w%f' | while read f; do
case "$f" in
*.php|*.sh|*.jsp|*.py) echo "可疑上传: $f" | mail -s "文件通道告警" you@example.com ;;
esac
done配合前面文章里提过的日志方案,把这条监听挂到 systemd 服务里常驻,就等于给文件通道加了一层守门人。
排查:连不上时该看哪几个地方
SFTP 这类通道一旦配错,报错信息往往很含糊,客户端只给你一句「连接被关闭」。这时候按下面的顺序从服务端日志往回推,基本五分钟能定位:
第一步,看 sshd 的日志而不是客户端的报错。journalctl -u ssh -n 50 --no-pager,或者老系统上看 /var/log/auth.log。真正的失败原因是记在服务端的。如果看到 bad ownership or modes for chroot directory component,就是前面说的属主问题;如果看到 fatal: subsystem request failed,就是 Subsystem 那行被注释了。
第二步,看是不是权限位太宽。SSH 对私钥文件极其挑剔,~/.ssh/id_ed25519 的权限必须是 600,~/.ssh 必须是 700。权限松了它直接拒绝使用并提示 Permissions 0644 are too open,而且这个检查在不同 OpenSSH 版本上的严格程度还不一样,老版本会放行、新版本直接拒绝,所以从旧机器迁移过来的密钥经常集体失效。
第三步,看是不是走了代理或者跳板。如果你本机配了 HTTP 代理,ssh 和 sftp 默认是不走代理的,但有些环境通过 ProxyCommand 做了转发,一旦跳板机上的连接被回收,表现就是连接建立成功后立刻断开。ssh -v file sync@host 加 -v 参数会把握手全过程打出来,在哪一步断掉一目了然,比反复改配置高效得多。
还有一个反直觉的点:SFTP 和 SCP 在新版 OpenSSH 里已经不是同一个实现了。OpenSSH 9.0 之后,scp 命令默认改用 SFTP 协议传输而不是老旧的 SCP 协议。这带来一个好处是 scp 现在也支持更大的文件和更好的错误提示,但代价是有些老服务器的 SFTP 子系统没配好时,以前能用的 scp 会突然报错。遇到这种情况的解法不是降级客户端,而是把服务端的 Subsystem 行补上——本质问题在服务端,客户端只是换了皮。
顺带一提容量问题。用 sftp 传大文件时,服务器端 /tmp 和用户配额同样要留意。某些配置下 SFTP 会先在临时目录组装再落盘,如果那个分区空间不足,表现是传到一半突然报 Failure 而不是 No space left on device,很容易被误判成网络问题。养成传之前先 df -h 看一眼的习惯,比事后排查省事得多。
小结
SFTP 不是新东西,但它被 scp 的便利性盖住了太久。把 sftp 子系统、chroot 隔离账号、sshfs 挂载、批处理续传这四件事配齐,你就得到一个既能人工拖拽、又能脚本自动化、还能断点恢复的文件通道,而且全程复用现有 SSH 端口,不增加任何对外暴露面。真正要紧的是记住那几个静默失效点:chroot 路径的属主要一路到 root、ForceCommand 和 ChrootDirectory 要成对出现、sshfs 要配 reconnect 心跳、批处理要加 BatchMode——这几个地方错了都不会报错,只会让你在某个深夜对着一动不动的终端发呆。把这些配好,剩下的就是安心用。