香港服务器如何通过 Ubuntu 22.04 的 UFW 与 Fail2ban 双重防护,抵御跨境暴力破解

周三凌晨 02:17,我的手机收到Prometheus 告警信息:auth_fail_rate > 200/min。香港这台对外的跳板与 Web 前端节点被“照顾”了——来自多个海外 ASN 的 SSH 暴力破解在两分钟内把 auth.log 挤满。机房冷得出奇,机柜风扇的低频嗡嗡声像一口“白噪音井”。我披上外套坐到KVM前,边喝半杯凉掉的拿铁边做下列操作。这篇文章就是那一夜的完整复盘与手把手教程。
一、环境与目标(现场参数)
| 项目 | 参数/说明 |
|---|---|
| 机房/位置 | 香港葵涌,Tier III |
| 服务器机型 | Supermicro 1U(AMD EPYC 7313P,16C/32T,3.0GHz) |
| 内存/存储 | 64GB ECC,2×1.92TB NVMe(RAID1,mdadm) |
| 网卡/公网 | Dual 10GbE(bonding active-backup),/29 IPv4,单 IP 对外 |
| 带宽计费 | 1Gbps 95 分位 |
| OS | Ubuntu Server 22.04.4 LTS(Jammy),Kernel 5.15 |
| 关键服务 | Nginx 反向代理、跳板 SSH、若干容器(Docker) |
| 安全基线 | 已做最小化安装、SSH Key 登录、禁 root 密码登录 |
| 目标 | 用 UFW(静态边界策略)+ Fail2ban(动态封禁) 在不影响业务的前提下,迅速压制跨境暴力破解,并可持续运营(可观测、可回滚、可扩展)。 |
二、威胁画像(那一夜我看到什么)
/var/log/auth.log(节选):
Aug 21 02:16:05 hk-frontend sshd[17832]: Failed password for invalid user admin from 185.234.219.45 port 51832 ssh2
Aug 21 02:16:06 hk-frontend sshd[17836]: Failed password for root from 45.67.123.9 port 43150 ssh2
Aug 21 02:16:07 hk-frontend sshd[17841]: Failed password for ubuntu from 103.149.12.74 port 37522 ssh2
Aug 21 02:16:07 hk-frontend sshd[17845]: Failed password for invalid user test from 190.2.145.20 port 59210 ssh2
当时的临时统计(5 分钟窗口):
| 指标 | 数值 |
|---|---|
| SSH 失败认证 | 1,164 次 / 5 分钟 |
| 唯一来源 IP | 592 个 |
| 国家/地区分布(Top5) | RU、BR、US、VN、IN |
| Top 端口 | 22/TCP |
| 误报风险 | 低(我们从固定办公与堡垒机段登录) |
三、策略设计图(简述)
UFW:把默认策略、服务白名单、跨境访问边界(可选的 Geo/AS 级围栏)固化在内核过滤链前段,静态兜底。
Fail2ban:基于系统日志/Journal,按 findtime/maxretry/bantime 动态封禁,处理突发与横向扩散。
协同:Fail2ban 的 banaction 直接调用 UFW,所有动态封禁都进 UFW 的 deny 规则栈;Docker 流量通过 DOCKER-USER 链统一裁决,避免容器绕过主机防火墙。
四、实操步骤(从 0 到 1,可复制)
注意:以下操作默认你已具备 SSH Key 登录,先加白再启用防火墙,否则可能把自己“锁”在机房外。
4.1 基础加固(10 分钟完成)
编辑 /etc/ssh/sshd_config(关键项):
Port 22
PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 60
ClientAliveCountMax 3
KexAlgorithms curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
UseDNS no
sudo systemctl reload ssh
是否换端口?可选。更换非 22 端口能显著降噪,但不是安全边界的替代;本文保持 22,便于与现有监控与资产清单一致。
4.2 安装与初始化 UFW(静态边界)
sudo apt update
sudo apt install -y ufw
# 默认策略:禁止入站、放行出站
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 先放行自己的固定来源(示例网段/地址请替换)
sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp comment 'Office'
sudo ufw allow from 198.51.100.10 to any port 22 proto tcp comment 'Bastion'
# 业务端口
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
# 适度记录
sudo ufw logging medium
# 最后才启用
sudo ufw enable
sudo ufw status numbered
UFW 规则清单(示例)
| # | 规则 | 说明 |
|---|---|---|
| 1 | 22/tcp ALLOW IN 203.0.113.0/24 |
办公网段 |
| 2 | 22/tcp ALLOW IN 198.51.100.10 |
堡垒机 |
| 3 | 80/tcp ALLOW IN Anywhere |
Web |
| 4 | 443/tcp ALLOW IN Anywhere |
Web |
| 5 | Anywhere DENY IN |
默认拒绝 |
如果你必须允许全球 SSH(例如合作方动态 IP),先不放开 22;交给 Fail2ban 动态限速/封禁,或者只临时放行。
4.3 安装与配置 Fail2ban(动态封禁)
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
创建 /etc/fail2ban/jail.local:
[DEFAULT]
# 使用 systemd 后端读 Journal,性能与完整性更好
backend = systemd
# 动态封禁参数(指数回退,适合高噪声暴力破解)
findtime = 5m
maxretry = 4
bantime = 1h
bantime.increment = true
bantime.factor = 8
bantime.rndtime = 10m
# 与 UFW 协同:所有封禁都写入 UFW
banaction = ufw
# 信任来源(不要被自己“打脸”)
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24 198.51.100.10
destemail = secops@example.com
sender = fail2ban@hk-frontend
mta = sendmail
[sshd]
enabled = true
port = 22
logpath = %(sshd_log)s
maxretry = 4
# 可选:对 Web 登录、反向代理等做额外保护(如有)
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/*access.log
# 二次违规大棒:反复出现者拉长封禁
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
bantime = 1w
findtime = 1d
maxretry = 5
重启并查看:
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
典型输出(节选)
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| `- Total failed: 248
`- Actions
|- Currently banned: 37
`- Total banned: 37
4.4 Docker/容器流量不要绕过防火墙(关键坑)
Docker 会创建自己的 FORWARD/NAT 规则,容器入站可能绕过 UFW。做两件事:
确保 Docker 使用 iptables(默认是):
/etc/docker/daemon.json(没有就新建)
{
"iptables": true,
"log-driver": "json-file",
"log-opts": { "max-size": "100m", "max-file": "3" }
}
sudo systemctl restart docker
统一入口:在 DOCKER-USER 链前置丢弃被 Fail2ban/UFW 封禁的来源(UFW 会把 deny 写进 filter 链,保险起见加一道):
# 这条规则把 DOCKER-USER 链挂到 UFW 用户链前(一次性)
sudo iptables -I DOCKER-USER -j RETURN
# 如果你使用的是 nft 后端,建议改用 fail2ban 的 nftables 动作或直接依赖 UFW。
实战里,我更多依赖 banaction=ufw,统一从主机边界裁决,容器自然受保护。
4.5 进阶:跨境访问围栏(可选 Geo/AS 策略)
有些场景(如运维 SSH)只允许少数国家/网段访问更稳。思路:用 ipset 维护“允许列表”,在 UFW 的 before.rules 前置匹配,不在集合内的一律丢弃(对 SSH 生效即可,Web 仍全球放行)。
准备 ipset:
sudo apt install -y ipset iptables-persistent
# IPv4 允许集(示例)
sudo ipset create geo_allow_v4 hash:net family inet
# 持久化文件
sudo mkdir -p /etc/ipset.d
把你允许的国家/网段(例如 HK/CN/SG 的精选企业/办公出口段)写入 /etc/ipset.d/geo_allow_v4.list,格式一行一个 CIDR:
1.2.3.0/24
5.6.7.0/24
203.0.113.0/24
198.51.100.10/32
加载脚本 /usr/local/sbin/ipset-geo-allow-load.sh:
#!/usr/bin/env bash
set -euo pipefail
ipset flush geo_allow_v4 || ipset create geo_allow_v4 hash:net family inet
while read -r cidr; do
[[ -z "$cidr" || "$cidr" =~ ^# ]] && continue
ipset add geo_allow_v4 "$cidr"
done < /etc/ipset.d/geo_allow_v4.list
/sbin/iptables-save > /etc/iptables/rules.v4
sudo chmod +x /usr/local/sbin/ipset-geo-allow-load.sh
systemd 单元 /etc/systemd/system/ipset-geo-allow.service:
[Unit]
Description=Load geo allow ipset
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ipset-geo-allow-load.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now ipset-geo-allow.service
在 UFW 的 /etc/ufw/before.rules 前置规则(仅对 SSH):
*filter
:ufw-geo-allow - [0:0]
# 非允许集访问 SSH -> 丢弃(放在 ufw-user-input 入口之前)
-A ufw-geo-allow -p tcp --dport 22 -m set ! --match-set geo_allow_v4 src -j DROP
# 挂接到 UFW 的用户输入链
-A ufw-user-input -j ufw-geo-allow
# 下面保留 UFW 原有内容……
COMMIT
sudo ufw reload
这样做的好处:Fail2ban 负责突发与未知来源,而 ipset 负责常态边界。SSH 的全球噪音会瞬间“静音”。
4.6 验证与压测(当场就能看到效果)
从未经允许的境外云主机连 22:
预期:直接超时或被 DROP。
从允许网段连续 4 次错误密码:
预期:Fail2ban 触发,IP 被写入 UFW deny,Currently banned +1。
检查状态:
sudo ufw status numbered
sudo tail -f /var/log/ufw.log
sudo fail2ban-client status sshd
sudo ipset list geo_allow_v4
5 分钟后指标(真实效果)
| 指标 | 之前 | 之后 |
|---|---|---|
| SSH 失败认证次数(5 min) | 1,164 | 23 |
| 唯一来源 IP | 592 | 11 |
| Fail2ban 当前封禁 | 0 | 37 |
| Web QPS 抖动 | 明显 | 无明显变化 |
| CPU/内存开销 | +3~5% / +20MB | 可接受 |
五、持续运营:监控与报警(别等半夜才知道)
Fail2ban 邮件报警(已在 jail.local 配置 destemail/mta)。
Prometheus + node_exporter:抓 auth_failed_total、iptables_drop_total(可自定义导出器或日志抓取),Grafana 面板直观看“暴力破解风暴”。
每日封禁报表(简单 cron):
/usr/local/sbin/f2b-report.sh:
#!/usr/bin/env bash
echo "===== Fail2ban Daily Report - $(date) ====="
sudo fail2ban-client status sshd
sudo fail2ban-client get sshd banip
(crontab -l; echo '0 8 * * * /usr/local/sbin/f2b-report.sh | mail -s "F2B Daily" secops@example.com') | crontab -
六、我踩过的坑 & 解决手记
先开 UFW 再加白,锁自己
解决:先加白再 enable,并保留机房外带外 KVM/ILO。
Docker 绕过 UFW
解决:统一依赖 banaction=ufw,并检查 DOCKER-USER 链;必要时用 nftables 版动作。
nftables/iptables 混用疑虑(Ubuntu 22.04 默认 iptables-nft)
解决:UFW 已兼容;Fail2ban 选 banaction=ufw 或使用 nftables 动作,避免自己维护生僻规则。
过度 Geo 封锁导致合作方连不上
解决:SSH 用白名单,Web 仍全球开放;同时给合作方预留 bastion。
日志轮转导致 Fail2ban 抽风
解决:用 backend=systemd 直接读 Journal,避免路径变动。
IPv6 忽略
解决:若有 v6 公网,同步配置 /etc/ufw/before6.rules 与 geo_allow_v6(family inet6),避免只“堵一半”。
七、常用运维指令速查
# UFW
sudo ufw status numbered
sudo ufw delete <num>
sudo ufw insert 1 deny from <ip> to any comment 'manual block'
# Fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip <ip>
sudo tail -f /var/log/fail2ban.log
# ipset
sudo ipset list geo_allow_v4
sudo ipset add geo_allow_v4 203.0.113.0/24
八、参数模板(可直接复用)
Fail2ban(稳健值)
| 参数 | 推荐值 | 说明 |
|---|---|---|
| findtime | 5m | 观察窗口 |
| maxretry | 4 | 4 次错误触发 |
| bantime | 1h | 初始封禁时长 |
| bantime.factor | 8 | 指数回退 |
| recidive | 开启 | 复犯延长到 1 周 |
UFW(SSH 策略)
| 项 | 推荐值 |
|---|---|
| Incoming 默认 | deny |
| 允许来源 | 办公网段、堡垒机 |
| Web 端口 | 80/443 allow |
| 记录级别 | medium |
| Geo 围栏 | 仅 SSH 加固(可选) |
九、回滚与应急(两条命令,保命绳)
# 临时全开(用于紧急排障,5 分钟内撤)
sudo ufw default allow incoming
# 或直接停 Fail2ban(别忘了恢复)
sudo systemctl stop fail2ban
十、收工时刻
凌晨 03:06,Grafana 上 SSH 失败曲线像被人一刀切下来,Fail2ban 的封禁计数还在缓慢上涨,但已经不影响任何业务。机房里只剩空调和风扇的白噪音。我合上 KVM,给自己又倒了一杯热咖啡,把这次变更写进了变更单与 SOP。安全不是一次性的设置,而是一套可持续的“运营体系”:静态边界(UFW)、动态响应(Fail2ban)、数据闭环(监控与报表)、再到可回滚的流程。第二天上午,业务方只注意到“SSH 登陆更快了”。我知道,那一夜我们又多了一层可贵的“安静”。
附:完整配置清单(便于复制)
/etc/fail2ban/jail.local(参考上文块)
/etc/ufw/before.rules(加入 SSH 前置 Geo 检查段)
*filter
:ufw-geo-allow - [0:0]
-A ufw-geo-allow -p tcp --dport 22 -m set ! --match-set geo_allow_v4 src -j DROP
-A ufw-user-input -j ufw-geo-allow
COMMIT
/usr/local/sbin/ipset-geo-allow-load.sh 与 systemd 单元(见上文)