改个 SSH 端口,我把自己锁在了服务器外面

本文 IP 与端口均已脱敏:服务器写作 203.0.113.7,新端口写作 34567

这本来是运维清单上最不起眼的一项:公网 VPS 的 22 端口天天被扫,把 sshd 挪到一个高位端口,降降 auth 日志的噪音。谁都会觉得这是个五分钟的活------改 sshd_config、重启、客户端配置加一行 Port,收工。

实际情况是:我先后翻车两次,第二次直接把保底的 22 也弄没了,人被锁在服务器外面,最后靠云厂商的网页 VNC 救回来。这篇复盘里有三个我以为自己懂、实际并不懂的知识点。

第一次翻车:改了 Port,端口纹丝不动

服务器端我做了教科书上写的一切:

perl 复制代码
# /etc/ssh/sshd_config
Port 34567
PasswordAuthentication no

reload 服务,客户端 ~/.ssh/configPort 改成 34567,云安全组放行。然后:

vbnet 复制代码
$ ssh myserver
ssh: connect to host 203.0.113.7 port 34567: Connection refused

第一反应是怀疑安全组。这个反应是错的,而且错得很典型。

Connection refused 和 timeout 是两种完全不同的故障。 refused 意味着 TCP 包已经到达了主机,内核回了 RST------"到了,但这个端口没人听";timeout 才是包在半路被丢了(安全组、防火墙 DROP)。所以 refused 恰恰证明安全组是好的,问题在服务器自己身上。

nc 把新旧端口各测一遍,一分钟就能把故障切到具体一层:

ruby 复制代码
$ nc -zv 203.0.113.7 34567
nc: connect ... port 34567 (tcp) failed: Connection refused   # 到了,没人听
$ nc -zv 203.0.113.7 22
Connection to 203.0.113.7 port 22 [tcp/ssh] succeeded!        # 旧端口还活着

旧端口还活着是个好消息:能进去查。

sshd 说它在听 34567,内核说它在听 22

上去之后看到了一组自相矛盾的现场:

perl 复制代码
$ sshd -T | grep ^port
port 34567                          # 配置解析:34567

$ ss -tlnp | grep sshd
LISTEN 0.0.0.0:22  users:(("sshd",...),("systemd",pid=1,...))   # 实际监听:22

注意 ss 输出的最后:这个监听 fd 的持有者里挂着 systemd(pid=1) 。这就是谜底------这台机器是 Ubuntu 24.04,而 Ubuntu 从 22.10 起,sshd 默认走 systemd socket activation :监听 socket 由 ssh.socket 这个 systemd 单元创建好再递给 sshd。端口的决定权根本不在 sshd_configPortListenAddress 整段被忽略。

日志里还有一条特别有迷惑性的证据:reload 之后 sshd 打出 Server listening on 0.0.0.0 port 22------它"重启"了,它"生效"了,只是监听的还是 systemd 塞给它的那个 22。

判别方法一条就够:

csharp 复制代码
$ systemctl is-enabled ssh.socket
enabled        # 是它,你改 sshd_config 的 Port 就是白改

顺手还掀开了第二块石头。sshd -T 里赫然写着 passwordauthentication yes------可我明明设了 no。原因是 OpenSSH 的配置是首值生效 ,而主配置第 12 行有一句 Include /etc/ssh/sshd_config.d/*.conf,cloud-init 生成的 50-cloud-init.conf 里的 PasswordAuthentication yes 永远排在我的 no 前面。一台公网机器,root 密码登录开着,auth 日志里全是各国 IP 的尝试记录。

这里的通用教训是:"我设了"和"生效了"是两回事,生效值只信 sshd -T,别信你改过的那个文件。

第二次翻车:自信地写下了埋葬自己的四行配置

既然端口归 ssh.socket 管,那就给它写个 drop-in。systemd 的 drop-in 是叠加语义,列表型选项要先用一个空赋值清掉原有累积,再声明新的:

ini 复制代码
# /etc/systemd/system/ssh.socket.d/listen.conf
[Socket]
ListenStream=          # 清空原单元的监听列表
ListenStream=22        # 过渡期保留旧端口
ListenStream=34567

daemon-reload、restart,然后在会话里 ss 一看,22 和 34567 都在监听。稳了,收工。

本地验证:34567,refused。心里一沉,再测 22------也是 refused

那一刻我意识到两件事:第一,这台服务器上现在没有任何端口在等我的 SSH;第二,我刚才那条"一切正常"的 ss 输出,是在一条 restart 之前就建立的旧会话里看的------restart 不会掐断已建立的连接,旧会话里岁月静好,说明不了新连接能不能进来。

回头逐行看那条 ss 输出,其实证据就在眼前:

ini 复制代码
LISTEN   [::]:22      ...
LISTEN   [::]:34567   ...

只有 [::]0.0.0.0 那两行不见了ListenStream= 只写端口号、不带地址时,systemd 只创建了 IPv6 的通配 socket,IPv4 流量按说可以靠 v4-mapped 兼容接住------但在这台机器上它没有生效。原来的 ssh.socket 里 22 是有一条独立 IPv4 socket 的,被我的清空重声明一并抹掉了。于是 IPv4 上 22 和 34567 都成了 refused,连保底通道一起陪葬。

两条硬结论:

  1. socket 单元里声明端口,v4 和 v6 显式各写一条,别赌 v4-mapped;
  2. 验证必须用新建的连接做,ss 输出必须逐行确认 0.0.0.0[::] 同时在场。

门外的人,盘点自己的口袋

被锁在外面之后,正确动作不是反复重试 ssh,而是冷静列一遍"手里还剩哪些不依赖 sshd 的通道":

通道 结果
443/80 通。说明机器和反代都活着,只是 SSH 的门锁了
内网穿透网关(Pangolin)预留的 raw TCP 入口 通。理论上可以在它的管理面板建一条资源代理回 127.0.0.1:22,从旁门进去------差点用上的骚路线
云厂商自动化助手(TAT,走云 API 免 SSH 执行命令) 本机没配 CLI 凭据,走不通
IPv6 直连 服务器没有公网 v6,走不通
云厂商网页 VNC 正解,永远走得通

最后在 VNC 里把 drop-in 改成显式双栈,一次生效。全程机器上的业务没有受任何影响------被锁住的只有我。

事后想,这张表其实应该在动手之前就列好。改远程 SSH 配置的第一步不是改配置,是确认至少有一条不依赖 sshd 的带外通道真的能用。

最终版脚本

完整、可直接抄的版本(Ubuntu 22.10+,root 执行;前提:云安全组已放行新端口,VNC 可用):

bash 复制代码
NEW_PORT=34567

# 端口归 ssh.socket 管;v4/v6 必须显式各写一条
mkdir -p /etc/systemd/system/ssh.socket.d
cat > /etc/systemd/system/ssh.socket.d/listen.conf <<EOF
[Socket]
ListenStream=
ListenStream=0.0.0.0:22
ListenStream=[::]:22
ListenStream=0.0.0.0:${NEW_PORT}
ListenStream=[::]:${NEW_PORT}
EOF
# 过渡期保留 22,从外部验证新端口后再删掉两条 22 重新应用

# sshd_config 同步改(对 socket activation 无效但防两处口径漂移);
# 压掉 cloud-init 的首值覆盖
sed -i "s/^#\?Port .*/Port ${NEW_PORT}/" /etc/ssh/sshd_config
printf 'PasswordAuthentication no\n' > /etc/ssh/sshd_config.d/50-cloud-init.conf

# 语法校验通过才应用;restart 不断已建立的会话
sshd -t
systemctl daemon-reload
systemctl restart ssh.socket ssh.service

# 验证:0.0.0.0 与 [::] 两行必须同时出现
ss -tln | grep ":${NEW_PORT} "

客户端只需要在 ~/.ssh/config 里加一行 Port 34567,scp、rsync、git 走别名自动继承。新端口首连会重新提示确认 host key------known_hosts[ip]:port 是独立条目,属正常现象。

迁移顺序的铁律:安全组放行新端口 → 服务器新旧双端口并行 → 从外部用新连接验证新端口 → 客户端切换 → 撤掉旧端口。 任何时刻手里都要有一条已验证可用的登录通道。

带走的清单

  1. Ubuntu 22.10+ 改 SSH 端口,改的是 ssh.socket 的 drop-in,不是 sshd_configsystemctl is-enabled ssh.socket 一条命令判别。
  2. ListenStream 显式写 0.0.0.0:[::]: 两条,纯端口号可能只给你 IPv6。
  3. 配置生效值信 sshd -T,实际监听信 ss -tlnp,两个都要看,它们可以不一致。
  4. Connection refused 查服务器监听,timeout 查安全组------判层先于排查。
  5. restart 后旧会话不断,一切验证用新建连接做。
  6. 动 SSH 配置之前,先确认一条不依赖 SSH 的带外通道。

这次的学费不贵:VNC 里十分钟。但同样的错误发生在一台没有带外控制台的机器上,就是另一个故事了。

相关推荐
大黄评测8 小时前
jQuery 遍历方法 each 实战:循环处理列表数据
后端
大勇前进9 小时前
jQuery 事件委托原理:彻底解决动态生成元素绑定失效问题
后端
苍何9 小时前
做了个微信聊天爆款视频 Skill, Codex / WorkBuddy一键复刻使用
后端·aigc
卷无止境9 小时前
用 FastAPI 撑起大文件的上传下载:从流式处理到断点续传的完整实践
后端·python·fastapi
铁皮饭盒9 小时前
网页端, 6.5MB人脸识别模型, 谷歌框架, 又快又准
前端·javascript·后端
刘某的Cloud9 小时前
Galera Cluster部署 mariadb 节点down机,log sequence number恢复
linux·运维·数据库·负载均衡·mariadb·集群·高可用
长大19889 小时前
jQuery AJAX 完整封装教程:告别重复写请求代码
后端
小此方9 小时前
Re:Linux系统篇(五十四)线程篇 · 七:互斥锁的底层实现原理:从硬件上下文、原子交换指令(Swap)到并发封装实战
linux·运维·驱动开发
worxfr10 小时前
Go 并发控制:从 Channel 方向约束到实战模式
开发语言·后端·golang
拾陆楼10 小时前
PT: DMSA辅助调tree报告前后级余量脚本
后端·学习