自己把自己关在门外:一次 UFW 把 SSH 锁死的排查记录

这次踩了一个挺经典的坑:我在云服务器上折腾 frp 的时候,顺手开了 UFW 防火墙,结果把自己 SSH 入口给挡住了。

服务器没死,服务也没坏,但我就是连不上。

最后发现:不是服务器出问题,是我自己把自己关在了门外。


事情背景

我有一台云服务器,上面跑了一些服务:

Caddy / Nginx
Docker
frp
一些 Go / Python / Node / Bun 服务

最近在折腾 frp,用来做内网穿透和服务分流。

因为涉及端口暴露,我当时想着:“要不顺手把 UFW 开起来,做一层本机防火墙?”

这个想法本身没错,但问题是:我这是一台云服务器。

云服务器本来就有一层安全组,比如阿里云安全组、腾讯云安全组、AWS Security Group 之类的。公网入口本来就可以在云后台控制。

结果我在云安全组之外,又叠了一层 UFW。

然后,事故来了。


现象:VNC 能进,SSH 进不去

最开始发现问题是:

SSH 22 端口连不上
但是云后台 VNC 可以连接

这就很有意思。

如果服务器彻底挂了,VNC 大概率也有问题。

但现在 VNC 能进,说明:

服务器还活着
系统还能登录
网络大体没死

问题就集中在:

sshd 服务
22 端口监听
服务器防火墙
云安全组
本地网络 / 代理

第一步:检查 ssh 服务

通过云后台 VNC 登录服务器后,先看 SSH 服务状态:

systemctl status ssh --no-pager

一开始看到的是:

ssh.service - OpenBSD Secure Shell server
Loaded: disabled
Active: inactive (dead)
TriggeredBy: ssh.socket

当时第一反应是:SSH 服务没起来。

于是启动它:

systemctl start ssh
systemctl enable ssh

再检查:

systemctl status ssh --no-pager

这次服务正常了:

Active: active (running)
Server listening on 0.0.0.0 port 22
Server listening on :: port 22

这说明 sshd 已经开始监听 22 端口。

再看端口:

ss -lntp | grep ':22'

确认 22 确实在监听。

但问题是:外面还是连不上。


第二步:本机测试 SSH

这时候要区分一个问题:

到底是 SSH 服务坏了,还是公网入口进不来?

在服务器 VNC 里执行:

ssh -v root@127.0.0.1

结果能进入 SSH 握手流程,出现类似:

Are you sure you want to continue connecting (yes/no/[fingerprint])?

这说明:

本机 SSH 是通的
sshd 没坏
22 端口本机可访问

所以问题不在 SSH 服务本身。

那就只剩下:

公网安全组
服务器防火墙
宝塔防火墙
本地网络 / 代理

第三步:破案,UFW 把我挡住了

继续检查防火墙:

ufw status

然后我突然想起来:我之前为了 frp,自己开过 UFW。

这下就破案了。

本质上是:

sshd 正常
22 端口正常监听
但是 UFW 没放行 22
所以公网 SSH 进不来

这就是典型的自己把自己锁在门外。

如果只是临时修复,可以执行:

ufw allow 22/tcp

然后再看:

ufw status verbose

但我这次的结论是:这台云服务器上暂时不需要 UFW。

因为公网入口已经由阿里云安全组控制了。

所以我直接关闭 UFW:

ufw disable

确认:

ufw status

看到:

Status: inactive

第四步:防止 UFW 重启后又复活

我一开始担心:现在是 inactive,那重启之后会不会又自己起来?

于是检查 systemd 状态:

systemctl is-enabled ufw

结果显示:

enabled

再看:

systemctl status ufw --no-pager

可以看到:

Loaded: loaded
Active: active (exited)

这里容易误解。

UFW 的 systemd 服务是 active (exited),不代表它像普通服务一样一直跑在后台。它更像是启动时加载规则,加载完就退出。

但既然我决定不用它,那就别留隐患。

先禁用开机启动:

systemctl disable ufw

如果想彻底防止它被误启动,还可以 mask:

systemctl mask ufw

执行后会看到类似:

Created symlink /etc/systemd/system/ufw.service → /dev/null.

这表示 UFW 服务被直接按住了。

再次检查:

systemctl is-enabled ufw

正常会显示:

masked

这下基本不用担心 UFW 自己复活了。


这次真正的问题不是 UFW,而是安全边界混乱

这次坑的本质不是 UFW 有问题。

UFW 是好东西。

问题是我在没有明确规划的情况下,把安全控制层叠得太多了:

阿里云安全组
服务器 UFW
宝塔防火墙
Docker 端口映射
Caddy / Nginx 反代
frp 端口转发

这些东西每一层都可能影响访问。

一旦出问题,排查复杂度会直接翻倍。

比如 SSH 连不上,到底是:

阿里云安全组没放?
UFW 拦了?
宝塔防火墙拦了?
sshd 没启动?
sshd 配置错了?
端口改了?
本地代理搞错了?

如果没有清晰的边界,就很容易排查到怀疑人生。


我现在更推荐的云服务器安全模型

对于云服务器,我现在倾向于这样分层:

公网入口控制:云厂商安全组
HTTP/HTTPS 入口:Caddy / Nginx
内网穿透:frp
业务服务:尽量只监听 127.0.0.1 或 Docker 内网
本机 UFW:暂时不用,除非规则非常清晰

也就是说,公网入口主要交给阿里云安全组。

阿里云安全组只放必要端口:

22    SSH,最好限制为自己的 IP
80    HTTP
443   HTTPS
7000  frps bindPort,按需开放

业务服务端口不要乱暴露公网。

例如:

8000
8120
8122
2368
3000

这些端口如果能走 Caddy 反代,就不要直接开放到公网。


frp 场景下端口应该怎么想

frp 很容易让人产生一个误区:

“我是不是要把所有业务端口都开放?”

其实不应该。

比较清晰的方式是:

公网只开放 Caddy 的 80/443
frps 只开放必要的 bindPort
具体业务服务通过域名分流

比如:

a.example.com -> Caddy -> frp -> 内网服务 A
b.example.com -> Caddy -> frp -> 内网服务 B
api.example.com -> Caddy -> frp -> 内网 API

这样公网入口还是统一的。

不需要每新增一个服务,就去云后台开放一个新端口。

更不需要在 UFW、宝塔、阿里云安全组里同时维护一堆端口。


以后开 UFW 的正确姿势

如果以后确实要用 UFW,正确顺序应该是:

ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status verbose

一定要先放行 SSH,再启用 UFW。

如果是远程服务器,最好先开一个备用登录方式,比如:

云后台 VNC
另一个 SSH 端口
云厂商救援模式

否则一条命令就可能把自己锁外面。

如果已经锁了,那就只能通过云后台 VNC 登录进去修。


常用排查命令整理

SSH 服务状态:

systemctl status ssh --no-pager

启动 SSH:

systemctl start ssh
systemctl enable ssh

检查 22 端口监听:

ss -lntp | grep ':22'

本机测试 SSH:

ssh -v root@127.0.0.1

查看 UFW 状态:

ufw status verbose

放行 SSH:

ufw allow 22/tcp

关闭 UFW:

ufw disable

禁用 UFW 开机启动:

systemctl disable ufw

彻底禁止 UFW 被启动:

systemctl mask ufw

恢复 UFW:

systemctl unmask ufw
systemctl enable ufw
ufw enable

查看 SSH 日志:

journalctl -u ssh --since "1 hour ago" --no-pager

实时看 SSH 日志:

journalctl -u ssh -f

这次的教训

这次最大教训是:

云服务器的安全入口一定要分层清楚。

不要一边用阿里云安全组,一边开 UFW,一边又用宝塔防火墙,然后 Docker 还暴露一堆端口。

不是不能这么做,而是你必须非常清楚每一层在管什么。

否则出问题的时候,你连门到底是被谁锁上的都不知道。

我这次就是典型案例:

服务器没死
SSH 没坏
22 端口也在监听
但公网就是连不上
最后发现是自己开的 UFW 拦住了

一句话总结:

我不是被黑客挡在门外的,是被自己挡在门外的。

这事挺尴尬,但也挺值。

因为修好的不只是 SSH,而是我对云服务器安全边界的理解。

Read more

三台机器部署 ClickHouse 高可用集群实战记录

本文是一份可发布版部署记录。真实 IP、域名、账号、密码、下载链接、业务目录名、机器唯一标识等敏感信息已经替换为占位符。命令中的 <...> 需要按自己的环境替换。 目标与拓扑 这次目标是用三台数据节点部署一套 ClickHouse 高可用集群,拓扑采用: 1 shard x 3 replicas 含义是:集群只有一个逻辑分片,三台机器都保存同一份数据的完整副本。任意一台数据节点宕机时,只要 ClickHouse Keeper 仍然有多数派,剩余节点仍可继续提供读写服务。 规划节点如下: 主机名示例地址角色ch-01<ch-01-ip>ClickHouse Server + ClickHouse Keeperch-02<ch-02-ip>ClickHouse Server + ClickHouse Keeperch-03<ch-03-ip&

By ladydd

折腾记(二):接入火山引擎实时语音 API,家庭语音助手体验直接拉满

接上篇 上一篇用全开源组件(Whisper + Hermes + Edge-TTS)搭了个语音助手,能跑,但体验就是"能用"二字: * 中文识别只有 70 分,方言基本歇菜 * 英文唤醒词"Alexa"喊着别扭 * 说完到回复要等 4-8 秒 * 它说话的时候你插不了嘴 这些问题靠堆开源组件很难根治。于是我去试了火山引擎(字节跳动)的语音服务,结果直接换了条路。 这篇分两段:先讲怎么用火山引擎的 ASR/TTS 替换掉开源组件(小改),再讲怎么上端到端实时语音模型(大改)。 第一段:先把 ASR 和 TTS 换成火山引擎 为什么换 我用豆包输入法的时候发现它语音识别准得离谱。一查,豆包用的就是字节自家的火山引擎 Seed-ASR。开通后有免费额度(

By ladydd

折腾记(一):用全开源组件给家里搭一个语音助手,对接自己的 Hermes Agent

起因 事情是从一块 ESP32-S3 开发板开始的。 我手上有一块 Seeed Studio XIAO ESP32-S3 Sense,带摄像头和麦克风。最初的想法很美好:用这块板子做一个无线语音终端,对着它说话,连到我服务器上跑的 Hermes Agent(一个自托管的 AI agent),让它回答我。 但折腾到一半我突然意识到一件事:我的麦克风、音响、服务器全在家里,为什么要绕一圈用 ESP32?直接把麦克风和音响插到服务器上不就行了? ESP32 那条路(做无线拾音终端)当然也有价值,但那是"为了学嵌入式而学",不是解决问题的最短路径。于是这个项目就从"嵌入式项目"变成了"在服务器上拼一个语音助手"。这篇就记录后者。 教训零:先想清楚你要解决的是什么问题。很多时候最优解比你最初设想的简单得多。 目标

By ladydd

Kiro 的三种代理设置方法:本地、服务端、Remote

作为kiro的骨灰级用户,这篇是我自己折腾 Kiro / Kiro Remote / Ubuntu Server 代理问题后的复盘。 核心不是“怎么配一个代理”,而是先判断:到底是谁在访问外网? 谁访问外网,代理就要配给谁。 0. 先说结论 Kiro 相关代理大概分三类: 场景真正访问外网的进程在哪里代理应该配在哪里本地 KiroWindows / Mac 本机本机 Clash / Proxifier / 系统代理服务端 Kiro / CLIUbuntu Server 上的 shell、CLI、node、kiro 进程Ubuntu 的环境变量,比如 HTTP_PROXY / HTTPS_PROXYKiro Remote远程 Ubuntu 上的 ~/.kiro-server 和 extensionHost远程 Ubuntu 的 Kiro Server

By ladydd
陕公网安备61011302002223号 | 陕ICP备2025083092号