rsync 自己不做身份认证,认证交给 SSH,所以「密钥对登录」就是把 ssh -i 私钥 塞进 rsync 的 -e。而 SSH 改端口则要看系统用的是传统 sshd.service 还是 systemd socket 激活——如果是后者,sshd_config 的 Port 是唯一真源,ssh.socket 由 sshd-socket-generator 自动派生。

本文记录一套完整可复现的流程,包括实测输出与排错表。

速查

场景 命令
rsync 走密钥对 rsync -avz -e "ssh -i ~/.ssh/key.pem" ./src/ root@IP:/dst/
写进 config 后 rsync -avz ./src/ aliyun:/dst/
改 SSH 端口 改 sshd_config 的 Port → sshd -t → daemon-reload → restart ssh.socket ssh.service
验证监听 systemctl show ssh.socket -p Listen + ss -tlnp | grep 端口
外部验证 本地 Test-NetConnection -ComputerName IP -Port 2233
失联兜底 阿里云控制台 VNC 远程连接(不走 SSH)

三条铁律

  1. 先开安全组,再改服务端,顺序反了就是自锁。
  2. 改完 sshd_config 必须先 sshd -t,配置写坏会导致 generator 不生成、静默回落 22。
  3. 永远保持至少一个非默认端口,否则可能生成空的 ListenStream= 导致无人监听。

1. rsync 使用密钥对

1.1 原理

rsync 是纯传输工具,不做认证。它把连接交给「远程 shell」(默认 SSH)。所以任何 SSH 的参数都能通过 -e 传进去:

rsync -avz --progress \
  -e "ssh -i ~/.ssh/aliyun.pem -o IdentitiesOnly=yes" \
  ./dist/ root@47.98.x.x:/var/www/html/
  • -o IdentitiesOnly=yes:防止 SSH 先去试 agent 里的其它 key,避免「公钥太多被拒」。
  • 改端口用 SSH 自己的 -p(小写),别和 rsync 的 -p/--perms 混淆:-e "ssh -p 2233 -i ~/.ssh/aliyun.pem"。

路径结尾的斜杠是 rsync 第一大坑

写法 含义
./dist/ 把 dist 里面的内容同步到目标目录
./dist 把 dist 这个目录本身放进目标目录,结果是 .../html/dist/...

1.2 推荐:写进 ~/.ssh/config

Host aliyun
    HostName 47.98.x.x
    User root
    Port 2233
    IdentityFile ~/.ssh/aliyun.pem
    IdentitiesOnly yes
    ServerAliveInterval 30
    ServerAliveCountMax 6

之后命令干净到只剩业务参数,ssh / scp / git 全部复用同一份配置:

rsync -avz --delete ./dist/ aliyun:/var/www/html/

1.3 私钥权限

控制台下载的 .pem 在 Windows 上通常是「所有人可读」,OpenSSH 直接拒绝:

Permissions for 'aliyun.pem' are too open.

WSL / Linux / macOS:

chmod 600 ~/.ssh/aliyun.pem    # 400 也可以

WSL 里的隐藏坑

私钥如果留在 /mnt/c/...,chmod 无效(DrvFs 固定 777)。必须拷进 Linux 文件系统:

mkdir -p ~/.ssh && cp /mnt/c/Users/you/Downloads/aliyun.pem ~/.ssh/ && chmod 600 ~/.ssh/aliyun.pem

原生 Windows OpenSSH(PowerShell):

icacls aliyun.pem /inheritance:r /grant:r "$($env:USERNAME):(R)"

1.4 阿里云侧的 5 个检查点

  • 登录名:Linux 默认 root;创建实例时选了非 root 登录名则是 ecs-user。用 ecs-user 写 /var/www 这类目录要提权:
    rsync -avz --rsync-path="sudo rsync" ./dist/ aliyun:/var/www/html/
  • 绑定密钥对需重启实例生效(控制台换绑/解绑同理)。
  • 安全组入方向放行 SSH 端口,授权对象最好只填自己的 IP。
  • 对端要装 rsync:rsync --version,没有就 sudo yum install -y rsync / sudo apt install -y rsync。
  • 密钥类型:控制台导入支持 rsa / dsa / ssh-rsa / ssh-dss / ecdsa(不含 ed25519)。用 ssh-keygen -y -f 私钥 反查公钥内容。

1.5 常用参数

参数 作用
-a 归档模式(递归 + 保留权限/时间/软链),基本必带
-v / -z 详细输出 / 传输压缩(文本划算,已压缩文件无益)
-P = --partial --progress,断点续传 + 进度,传大文件必带
-n dry-run,改 --delete 前一定先跑一次
--delete 目标端删除源端已不存在的文件(镜像同步,危险)
--exclude='.git' / --exclude-from=file 排除
--bwlimit=2M 限速
--rsync-path="sudo rsync" 对端用 sudo 跑 rsync

2. SSH 改端口

2.1 先判断服务模式

systemctl status ssh.socket ssh.service 2>/dev/null
systemctl cat ssh.socket
ss -tlnp | grep -E 'sshd|:22\b'
模式 特征 谁监听 改哪里
传统 只有 sshd.service / ssh.service 常驻 sshd 自己 sshd_config 的 Port
socket(Accept=yes) 有 ssh@.service,每个连接一个进程 systemd ssh.socket 的 ListenStream
socket(Accept=no) ssh.service 常驻,TriggeredBy: ssh.socket systemd 建 socket,fd 传给 sshd 看有没有 generator ↓

Accept=no 的指纹:ss -tlnp 里同一个 socket 同时挂在 systemd(pid 1) 和 sshd 上:

LISTEN 0 4096 0.0.0.0:2233 0.0.0.0:* users:(("sshd",pid=1269022,fd=3),("systemd",pid=1,fd=76))
                                                    ↑ 共享 fd,不是各自监听

2.2 决定性诊断:有没有 sshd-socket-generator

ls -l /usr/lib/systemd/system-generators/ /lib/systemd/system-generators/ 2>/dev/null | grep -i ssh
ls -l /run/systemd/generator/ssh.socket.d/ 2>/dev/null
sudo sshd -T | grep -E '^(port|listenaddress)'
  • 有 sshd-socket-generator → 改 sshd_config 就够了(generator 自动翻译成 ListenStream)
  • 没有 → 必须手改 ssh.socket

2.3 自动同步链路(有 generator)

操作(三步)

# 1. 改配置(drop-in 优先于主文件,因为 Include 在顶部,sshd 是"先出现者生效")
sudo mkdir -p /etc/ssh/sshd_config.d
printf 'Port 2233\n' | sudo tee /etc/ssh/sshd_config.d/99-ssh-port.conf

# 2. 校验 + 触发 generator
sudo sshd -t
sudo systemctl daemon-reload

# 3. 看生成结果(关键验证)
cat /run/systemd/generator/ssh.socket.d/addresses.conf

# 4. 重启
sudo systemctl restart ssh.socket ssh.service

生成结果应形如:

# Automatically generated by sshd-socket-generator

[Socket]
ListenStream=
ListenStream=0.0.0.0:2233
ListenStream=[::]:2233

2.4 手改 ssh.socket(无 generator)

sudo systemctl edit ssh.socket
[Socket]
ListenStream=
ListenStream=0.0.0.0:2233
ListenStream=[::]:2233

必须写两条 ListenStream

若基础单元含 BindIPv6Only=ipv6-only,[::] 那条 socket 不接收 IPv4 连接,基础单元靠显式多写一条 0.0.0.0:22 补齐 IPv4。

只写一条 ListenStream=2233 → 变成「仅 [::]:2233 且 ipv6-only」→ IPv4 全部失联,且表象是「改了端口但连不上」,极难排查。

  • 第一行空值 ListenStream= 是清空继承的监听列表,不写则新旧端口同时监听。
  • BindIPv6Only / FreeBind 不用重写,drop-in 只覆盖显式设置的指令。
  • 过渡期想双端口并存:别写清空行,直接写四条 ListenStream。

2.5 生效与验证

sudo systemctl restart ssh.socket ssh.service   # reload 无效!见下

systemctl show ssh.socket -p Listen
ss -tlnp | grep 2233
sudo sshd -T | grep -E '^(port|listenaddress)'

systemctl reload ssh 永远不会改端口

reload 只是发 SIGHUP 让 sshd re-exec,而复用的是 systemd 给的那个 fd。必须 restart socket。

外部验证(ss 只证明服务端在听,证明不了安全组放行):

Test-NetConnection -ComputerName 47.98.x.x -Port 2233
ssh -p 2233 -i ~/.ssh/aliyun.pem root@47.98.x.x

在服务器上 ssh -p 2233 root@127.0.0.1 成功没有意义,那走 lo 接口,完全绕过安全组。

2.6 收尾:关掉旧端口

sudo grep -rn -E '^\s*Port\s' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/   # 先定位
sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)         # 备份
# 注释掉 Port 22,只留 Port 2233
sudo sshd -t && sudo systemctl daemon-reload
cat /run/systemd/generator/ssh.socket.d/addresses.conf                       # 确认只剩 2233
sudo systemctl restart ssh.socket ssh.service

验证新连接稳定后,再回阿里云控制台删掉 22 的安全组规则。

2.7 安全性检查

systemctl cat ssh.service | grep -iE 'killmode|execstart'   # KillMode=process → 重启不踢现有会话
sudo ufw status

ufw status: inactive 的含义

主机侧没有任何防火墙,访问控制 100% 依赖阿里云安全组。云上常见做法,但要知悉。

将来若启用 ufw,必须先放行新端口再 enable:

sudo ufw allow 2233/tcp
sudo ufw enable    # 最后一步

其他连带影响

  • 阿里云控制台 Workbench 远程连接走 SSH 22,改端口后失效(部分版本可指定端口)。
  • VNC 管理终端不走 SSH,永远可用——唯一的兜底通道。
  • 别忘了:fail2ban 的 port、CI/CD 部署脚本、监控探针、Ansible 的 ansible_port、宝塔面板、iptables 旧规则。

2.8 排错表

现象 原因
改了 sshd_config 的 Port 没反应 socket 激活下 Port 由 generator 翻译,需 daemon-reload;无 generator 则要手改 ssh.socket
新旧端口同时监听 drop-in 里漏写了清空行 ListenStream=
改了端口但 IPv4 连不上 BindIPv6Only=ipv6-only 下只写了 [::],漏写 0.0.0.0:
改了配置后完全没监听 sshd -t 失败 → generator 不生成 → 空 ListenStream= 清空了列表
reload 后端口没变 正常,必须 restart ssh.socket
ssh.socket 起不来 Address already in use 旧进程占用,或 drop-in 端口重复
安全组加了还是连不上 ufw 没放行 / 授权对象 IP 不对(家宽 IP 会变)
Workbench 连不上 走 SSH 22,改用 VNC
no mutual signature algorithm 老服务端 + RSA/SHA-1:-o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa
Permission denied (publickey) 用户名错 / 公钥没进 authorized_keys / 用错私钥

2.9 兜底与回滚

阿里云控制台 → VNC 远程连接登录:

sudo cp -a /etc/ssh/sshd_config.bak.$(date +%F) /etc/ssh/sshd_config
sudo sshd -t
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket ssh.service

或删掉手写的 drop-in:

sudo rm -f /etc/systemd/system/ssh.socket.d/*.conf /etc/ssh/sshd_config.d/99-ssh-port.conf
sudo systemctl daemon-reload && sudo systemctl restart ssh.socket ssh.service

ConditionPathExists=!/etc/ssh/sshd_not_to_be_run:sudo touch /etc/ssh/sshd_not_to_be_run 可让 sshd 彻底不监听。

3. 本机实测记录

3.1 环境事实

/usr/lib/systemd/system/ssh.socket:

[Unit]
Description=OpenBSD Secure Shell server socket
Before=sockets.target ssh.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

[Socket]
ListenStream=0.0.0.0:22
ListenStream=[::]:22
BindIPv6Only=ipv6-only
Accept=no
FreeBind=yes

[Install]
WantedBy=sockets.target
RequiredBy=ssh.service

ssh.service 显示 disabled 但 active (running)、TriggeredBy: ● ssh.socket——这是正常的,不是配置坏了。

RequiredBy=ssh.service 是 [Install] 指令,enable 时会在 /etc/systemd/system/ssh.service.requires/ 建符号链接,于是 ssh.service 反向依赖 ssh.socket。

因此不推荐简单的 disable ssh.socket

顺序必须是:disable ssh.socket → 确认 .requires 链接消失 → enable/restart ssh.service。只 mask ssh.socket 而不清依赖链接,ssh.service 会因缺 Requires 直接启动失败。

systemctl disable ssh.socket
ls -l /etc/systemd/system/ssh.service.requires/   # 应无 ssh.socket
sudo systemctl enable --now ssh.service
systemctl show ssh.service -p Requires

3.2 诊断输出

$ ls -l /usr/lib/systemd/system-generators/ | grep -i ssh
-rwxr-xr-x 1 root root 482376 Sep  1 02:38 sshd-socket-generator

$ ls -l /run/systemd/generator/ssh.socket.d/
-rw-r--r-- 1 root root 169 Oct 10 19:55 addresses.conf

$ sudo sshd -T | grep -E '^(port|listenaddress)'
port 22
port 2233
listenaddress [::]:22
listenaddress 0.0.0.0:22
listenaddress [::]:2233
listenaddress 0.0.0.0:2233

$ cat /run/systemd/generator/ssh.socket.d/addresses.conf
# Automatically generated by sshd-socket-generator

[Socket]
ListenStream=
ListenStream=0.0.0.0:2233
ListenStream=[::]:2233
ListenStream=0.0.0.0:22
ListenStream=[::]:22

$ systemctl show ssh.socket -p Listen
Listen=0.0.0.0:2233 (Stream)
Listen=[::]:2233 (Stream)
Listen=0.0.0.0:22 (Stream)
Listen=[::]:22 (Stream)

$ ss -tlnp | grep 2233
LISTEN 0 4096 0.0.0.0:2233 0.0.0.0:* users:(("sshd",pid=1269022,fd=3),("systemd",pid=1,fd=76))
LISTEN 0 4096   [::]:2233   [::]:* users:(("sshd",pid=1269022,fd=4),("systemd",pid=1,fd=77))

结论:sshd_config → generator → ssh.socket 自动同步链路验证通过。此后改端口只需改 sshd_config 一处。

3.3 待办清单

  • 本地 Test-NetConnection -ComputerName <IP> -Port 2233 通
  • 阿里云安全组确认有 TCP 2233(授权对象限自己 IP)
  • sshd_config 注释掉 Port 22
  • sshd -t → daemon-reload → 确认 addresses.conf 只剩 2233 → restart
  • 本地 ssh -p 2233 成功
  • 删除安全组 22 规则
  • 更新本地 ~/.ssh/config 的 Port 2233
  • 检查 fail2ban(若有)的 port 设置
  • 确认阿里云 Workbench 是否需要改用 VNC

4. 参考