作者:Felix (Haro 协作整理)
前几天一台自托管服务器突然不可用。服务本身不算大,平时也很安静,所以这种“突然挂一下,重启又好了”的情况反而最让人不踏实:它到底只是被流量打懵了,还是哪里真的出了安全问题?
这台机器承担的是比较典型的小型公网服务角色:公开 Web 站点、WebDAV 文件同步入口、带二次认证的管理入口、MQTT TLS 服务,以及 VPN / 组网控制面。单看每个服务都不复杂,但它们叠在一起之后,就会形成一个不小的公网暴露面。
这次我没有只把机器重启完就收工,而是顺手做了一轮小型复盘:先确认有没有明显入侵迹象,再重新盘点端口暴露面,最后用安全组收敛、nginx 限流和 fail2ban,把几个容易被扫的入口补上基础防护。
第一件事不是改配置
异常发生后的第一反应很容易是重启、改配置、升级服务。但如果还没有保留现场信息,后面就很难判断原因。重启可以恢复服务,却也会冲掉一部分排查线索。
我先看了几个基础维度:
journalctl --since "today"
systemctl --failed
ss -lntup
df -h
free -h
top
这里的目标不是立刻定位到某一条日志,而是先把问题分成几类:
- 系统资源是否被打满,例如内存、磁盘、进程数或连接数。
- 服务进程是否异常退出,是否有 systemd failed unit。
- 公网入口是否出现大量扫描、认证失败或协议探测。
如果这一步发现磁盘满、OOM、数据库崩溃,处理方向会完全不同。对我来说,这一轮检查的价值在于先把“系统自己坏了”和“公网入口被打了”分开,不然很容易一边猜一边改。
先确认有没有被打进去
公网服务器被扫描是常态,不是例外。Web 日志里经常能看到这类路径:
/.env
/.git/
/wp-config.php
/xmlrpc.php
/server-status
/actuator
/HNAP1
这些请求说明有人在枚举常见漏洞、弱配置或框架默认入口。但“有人扫你”不等于“已经打进来了”。真正需要关注的是有没有进一步证据。
我重点检查了这些位置:
last
lastb
journalctl -u ssh --since "24 hours ago"
getent passwd
crontab -l
systemctl list-timers
systemctl list-units --type=service --state=running
判断是否入侵成功,主要看这些信号:
- 是否有异常登录成功。
- 是否新增了陌生用户、陌生 SSH key 或异常 sudo 记录。
- 是否出现未知的 systemd service、timer 或 cron。
- 是否有未知进程监听公网端口。
- 是否有 Web shell、异常二进制或反连进程。
- 业务配置、数据库或站点文件是否出现非预期修改。
这次没有发现明确入侵成功的证据。这个结论让我放心了一些,但也不代表可以什么都不做。更准确地说,这台机器遇到的是持续公网扫描和协议探测,部分入口存在被异常流量拖垮的风险。
把公网暴露面重新摊开看
确认没有明显入侵之后,我开始重新盘点公网暴露面。这里最怕的是“以为自己只开了几个端口”,但云安全组、本机监听和实际应用防护并没有对齐。
我先对齐了三张表:
- 云厂商安全组到底放行了哪些端口。
- 服务器本机实际监听了哪些端口。
- 每个公网入口有没有认证、限流和封禁策略。
本机可以先从监听端口和 fail2ban 状态看起:
ss -lntup
fail2ban-client status
盘点时,我把端口分成几类处理:
- Web / HTTP 服务:例如 80、443、WebDAV,适合 nginx 限流加 fail2ban。
- 登录和认证入口:例如 SSH、Authelia、VPN,适合强认证加 fail2ban。
- MQTT 等协议服务:适合 TLS、账号认证,再按协议错误或连接爆发做封禁。
- UDP 组网协议:例如 STUN、ZeroTier、WireGuard 一类,更多依赖协议密钥和安全组控制。
- 管理面端口:例如 metrics、admin gRPC、Web UI,优先不公网开放。
整理完之后会发现,有些端口根本不该进入 fail2ban 讨论。能不暴露公网的管理面端口,就不要暴露公网;真正需要公网访问的协议入口,再考虑限流和动态封禁。
第一轮:关闭不该公网开放的端口
比起给所有端口都加 fail2ban,更重要的是先减少暴露面。这次我先做了几项收敛:
- SSH 不再直接暴露公网,管理操作改走 VPN 通道。
- 管理 UI 不暴露公网,需要时通过 VPN 或 SSH tunnel 访问。
- metrics、debug、admin gRPC 这类管理端口从公网安全组移除。
- 组网协议只保留必要 UDP 入口。
- 对一些 TCP fallback 入口先关闭观察,确认没有正常业务依赖后再长期收敛。
这一步的收益很直接:扫描面小了,误封风险低了,后面要维护的 fail2ban 规则也少了。安全配置不是越多越好,能关掉的入口,比写一条复杂规则更可靠。
第二轮:给 Web 层加限流
剩下确实需要公网访问的服务,才进入限流和封禁这一步。Web、WebDAV 和认证入口都在反向代理后面,适合先用 nginx 做基础限流。一个简化示例如下:
limit_req_zone $binary_remote_addr zone=web_per_ip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=per_ip_conn:10m;
server {
listen 443 ssl;
limit_req zone=web_per_ip burst=100 nodelay;
limit_conn per_ip_conn 16;
location / {
proxy_pass http://backend;
}
}
限流不能拍脑袋。比如 WebDAV 客户端同步大量小文件时,请求数会很密集,如果简单设置成 3r/s,可能不是防攻击,而是在封自己的正常同步。
我的处理思路是:
- 管理入口可以保守一些。
- WebDAV / 同步类流量要宽松一些,避免影响正常客户端。
- 根路径和无效路径尽量快速返回 404。
- 明显扫描路径可以直接拒绝,并交给 fail2ban 做后续封禁。
第三轮:让 fail2ban 处理重复噪声
到这里才轮到 fail2ban。它不是 WAF,也不是万能防火墙;它最适合处理的是“同一个来源反复触发明确失败特征”的场景,例如:
- 同一 IP 反复访问敏感扫描路径。
- SSH、VPN、认证入口持续失败。
- 协议端口收到大量错误协议探测。
- nginx 限流命中后仍然持续重试。
安装很简单:
apt install fail2ban
systemctl enable --now fail2ban
我把本机自定义规则单独放在这些位置,避免混进发行版默认配置:
/etc/fail2ban/jail.d/public-services.local
/etc/fail2ban/filter.d/custom-*.conf
后续维护时,只看这两处就能知道自己加了哪些规则。这个习惯很重要,否则过几个月之后,很难分清哪些是系统默认配置,哪些是自己临时补的规则。
几个 jail 的设计思路
Web 扫描
Web 扫描 jail 的目标是封禁反复访问敏感路径的 IP,而不是把所有 404 都当攻击。示例:
[web-scan]
enabled = true
filter = web-scan
journalmatch = CONTAINER_NAME=web
port = http,https
maxretry = 8
findtime = 10m
bantime = 6h
过滤器只匹配很明确的扫描路径:
[Definition]
failregex = ^.*client: <HOST>, .*request: "(?:GET|POST|HEAD) /(?:\.env|\.git/|wp-config\.php|xmlrpc\.php|actuator|server-status).*$
ignoreregex =
WebDAV 扫描
WebDAV 要特别谨慎。同步客户端会产生大量 PROPFIND、GET、PUT 请求,不能简单把频率等同于攻击。
[webdav-scan]
enabled = true
filter = webdav-scan
journalmatch = CONTAINER_NAME=webdav
port = 5007
maxretry = 6
findtime = 10m
bantime = 6h
这里我只抓明显不属于业务的扫描路径。一个实际踩坑是:不要轻易把看起来“像攻击”的业务路径写进规则。例如某些管理界面会正常使用 /cgi-bin/,如果把它当通用扫描特征,很容易误封自己。
认证入口
认证入口适合针对清晰的登录失败日志加 jail:
[auth-fail]
enabled = true
filter = auth-fail
journalmatch = CONTAINER_NAME=auth-service
port = 443
maxretry = 5
findtime = 10m
bantime = 6h
这个规则的关键是不要匹配正常未登录跳转,也不要误伤二次认证流程。只抓真正失败的认证事件。
MQTT
MQTT 这类协议服务不适合按 HTTP 路径判断,但适合抓协议错误、认证失败和连接爆发:
[mqtt-protocol]
enabled = true
filter = mqtt-protocol
journalmatch = _SYSTEMD_UNIT=mosquitto.service
port = 8883
maxretry = 6
findtime = 10m
bantime = 2h
如果要做连接突增规则,阈值应该留得宽一点。正常客户端重连、网络抖动、证书更新后批量重连,都可能在短时间内制造连接峰值。
HTTPS 协议探测
对某些 HTTPS 协议入口,可以抓明显错误握手:
[headscale-handshake]
enabled = true
filter = headscale-handshake
journalmatch = CONTAINER_NAME=headscale
port = 443
maxretry = 12
findtime = 10m
bantime = 2h
典型匹配对象包括:
client sent an HTTP request to an HTTPS server
unsupported SSLv2 handshake
TLS handshake unexpected EOF
这类入口的阈值不能太低。公网 HTTPS 偶发握手失败很常见,fail2ban 应该处理持续探测,而不是处理每一个噪声事件。
OpenVPN
OpenVPN TCP 端口经常会被 HTTP 或 TLS 扫描打到,日志里会出现协议层异常。示例 jail:
[openvpn-protocol]
enabled = true
filter = openvpn-protocol
journalmatch = _SYSTEMD_UNIT=openvpn@server.service
port = 1194
maxretry = 6
findtime = 10m
bantime = 2h
过滤器示例:
[Definition]
failregex = ^.*<HOST>:\d+ WARNING: Bad encapsulated packet length from peer .*$
^.*TLS Error: unknown opcode received from \[AF_INET\]<HOST>:\d+ .*$
^.*<HOST>:\d+ TLS Auth Error:.*$
^.*<HOST>:\d+ VERIFY ERROR:.*$
^.*<HOST>:\d+ Authenticate/Decrypt packet error:.*$
ignoreregex =
每条规则都要先验证
fail2ban 最容易犯的错,是写完正则直接启用。它看起来只是一个小工具,但正则一旦写错,就可能把正常用户、正常客户端,甚至自己的出口 IP 封掉。我的做法是每加一个 filter,都先跑两步。
第一步是配置检查:
fail2ban-client -t
第二步是拿历史日志回放:
fail2ban-regex /tmp/sample.log /etc/fail2ban/filter.d/filter-name.conf --print-all-matched
这里重点看四件事:
- 是否命中预期攻击日志。
- 是否误命中正常业务日志。
- 是否可能命中自己的公网出口 IP。
- maxretry、findtime、bantime 是否太激进。
这次最重要的经验是:正则规则要从业务语义出发,而不是只看路径像不像攻击。某个路径在陌生站点上可能是扫描特征,在自己的服务里可能就是正常业务路径。fail2ban 的价值是过滤重复噪声,不是替我做业务判断。
最终形成的防护分层
处理完之后,这台服务器的公网防护大概分成五层。它们不复杂,但每层负责的事情比较清楚:
- 云安全组:关闭不该公网开放的管理端口。
- 服务认证:SSH、VPN、认证入口、MQTT、WebDAV 各自保留强认证。
- 反向代理:对 Web 和管理入口做基础限流。
- fail2ban:对扫描、认证失败和异常协议流量做动态封禁。
- 日志复盘:持续观察误封、漏封和封禁数量。
不是所有公网入口都适合 fail2ban。例如 STUN UDP、ZeroTier / WireGuard 一类 UDP 组网协议,或者没有稳定失败日志的二进制协议,更适合依赖协议自身密钥、安全组最小开放、版本更新和流量监控。
复盘结论
这次事件最后的结论不是“服务器被入侵了”,而是:服务器遭遇了持续公网扫描和协议探测,暴露面里有一些入口值得收敛,部分服务需要动态封禁来降低被异常流量拖垮的风险。
这也是我觉得这次折腾值得记录下来的原因。公网服务的很多问题不是一次性的大事故,而是由长期存在的小暴露面、小默认配置和持续扫描慢慢累积出来的。
这次处理带来的收益也比较明确:
- 确认没有明显入侵成功证据。
- 识别出持续公网扫描和协议探测。
- 收敛了管理端口暴露。
- 对 Web、WebDAV、MQTT、认证入口和 VPN 增加了 fail2ban。
- 修正了可能误封正常业务的规则。
- 建立了后续观察口径。
一句话总结就是:不是所有异常都是入侵,但所有公网异常都值得顺手复查暴露面。
后续还可以继续做
fail2ban 只是这次的小闭环,后续还有几个方向可以继续补:
- 定期导出云安全组,和本机监听端口做 diff。
- 给关键 jail 加告警,汇总 active banned 和 total banned。
- 把 fail2ban 封禁记录纳入每日安全日报。
- WebDAV 使用独立账号和最小权限。
- 给重要服务增加 systemd 资源限制,避免扫描流量拖垮整机。
- 给自托管组件建立固定升级节奏。
公网服务没有“一次加固,永久安全”的状态。比较现实的做法,是持续缩小暴露面,把失败日志变成可观察对象,再让工具自动处理那些重复、明确、低价值的探测流量。