香港服务器运行 Ubuntu 22.04 LTS 时,如何用 UFW + Fail2ban 搭一套“多层次安全防护”

那天凌晨 2:30,香港葵涌机房的空调有点过冷,我一边搓手取暖,一边盯着监控屏幕上突然飙升的 SSH 失败登录数。香港节点公开在外网,亚洲段的扫描像潮水一样涌来。
我做了很多年的运维,知道只靠一层防线是扛不住的。于是,这篇笔记就从那一夜开始:我在一台跑着 Ubuntu 22.04 LTS 的香港服务器上,落地了 UFW(Uncomplicated Firewall) 与 Fail2ban 的组合拳,做出了一套可复用、可扩展、可回滚的多层次安全防护方案。下面把我在现场的完整过程、踩过的坑、背后的原理与细节,都交代清楚。
1. 现场环境与目标
1.1 硬件与网络(实配示例)
| 项 | 参数 |
|---|---|
| 机房 | 香港葵涌 Tier III 机房,冷通道封闭 |
| 服务器 | 1U 双路(示例):Xeon Silver 4210R ×2 / 64GB DDR4 ECC / NVMe U.2 1.92TB ×2(RAID1,系统盘) |
| 网卡 | Intel X710 双口 10GbE(Bonding mode=active-backup),BMC 独立管理口 |
| 操作系统 | Ubuntu Server 22.04.4 LTS(最小化安装) |
| 外网 | BGP 线路,IPv4 /29,IPv6 /64 |
| 管理方式 | IPMI(上锁)、KVM over IP 备用 |
| 应用 | Nginx 反向代理 + API 服务(容器化),SSH 仅限跳板机/办公网段 |
说明:不一定非要跟我一模一样的配置;你只需要对照自己的实际环境,把下面的策略逐条落地。
1.2 防护目标(分层思路)
| 层次 | 组件/手段 | 目的 |
|---|---|---|
| 周边层 | 机房 ACL / 云安全组(若有) | 粗粒度拉黑已知恶意段,限制管理网段 |
| 主机网络层 | UFW | 默认拒绝,最小开放,速率限制,白名单优先 |
| 应用行为层 | Fail2ban | 依据日志行为实时拉黑(动态),遏制暴力破解与路径探测 |
| 运维流程层 | 变更-验证-回滚 | 防误封、防锁死、可观测、可审计 |
2. 变更前的“自保”准备
真正的事故,多半发生在“以为没事”的那一刻。任何安全策略上线前,我都先给自己留条回退路。
确认有带外通道(BMC/KVM over IP 可用),别把自己关在门外。
开两条 SSH 会话:A 会话做变更;B 会话只做监控(journalctl -f、tail -f /var/log/auth.log)。
预设自动回滚保险(万一把 SSH 挡了):
# 5 分钟后自动关闭 UFW(如果我没手动取消)
echo "ufw disable" | sudo at now + 5 minutes
变更完成确认无误,再用 atrm <jobid> 取消定时。
快速梳理当前开放端口:
sudo ss -tulpn
sudo ufw status verbose || true
3. 系统基线加固(在上墙前做地基)
# 更新补丁与必需工具
sudo apt update && sudo apt -y upgrade
sudo apt install -y vim htop curl git net-tools lsof tmux
# 时区与 NTP
sudo timedatectl set-timezone Asia/Hong_Kong
sudo apt install -y chrony
sudo systemctl enable --now chrony
# 新建运维账户,禁用 root 远程密码登录
sudo adduser opsadmin
sudo usermod -aG sudo opsadmin
sudo passwd -l root
SSH 加固(保留你已有的跳板机/办公网段,别一刀切):
sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)
sudo vim /etc/ssh/sshd_config
# 关键项建议
# Port 22 # 如需改端口,可改为 2222,但不要只靠“换端口”当安全
Protocol 2
PermitRootLogin no
PasswordAuthentication no # 强烈建议使用密钥登录
PubkeyAuthentication yes
KbdInteractiveAuthentication no
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 20
MaxAuthTries 3
# AllowUsers opsadmin # 可结合跳板机:from="x.x.x.x/32" 限制公钥
生效与验证:
sudo systemctl restart ssh
# 在第二个终端用新会话登录验证成功后,再继续下面步骤
4. UFW:把“该开的”只开到“该开的”程度
4.1 安装与全局设置
sudo apt install -y ufw
# Ubuntu 22.04 默认 nftables 后端,兼容即可
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
4.2 白名单优先、再开放服务
假设:SSH 用 22/TCP(或 2222/TCP),Nginx 提供 80/443,Prometheus Node Exporter 用 9100,业务 API 8080 仅内网访问。
# 先放行我的办公网与跳板机(示例)
sudo ufw allow from 203.0.113.10 to any port 22 proto tcp comment 'JumpHost'
sudo ufw allow from 198.51.100.0/24 to any port 22 proto tcp comment 'OfficeNet'
# 如果改了端口,例如 2222
# sudo ufw allow from 203.0.113.10 to any port 2222 proto tcp
# 公网服务
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
# 仅内网/监控用(示例:监控网段 10.10.0.0/16)
sudo ufw allow from 10.10.0.0/16 to any port 9100 proto tcp comment 'NodeExporter'
# 启用速率限制:对 SSH 做连接速率限制(守住爆破)
sudo ufw limit 22/tcp comment 'SSH rate limit'
# 如果你使用 2222,改成相应端口
# 启用并查看
sudo ufw enable
sudo ufw status verbose
常见端口策略表(示例)
| 端口 | 方向 | 来源 | 策略 | 备注 |
|---|---|---|---|---|
| 22/tcp | 入站 | 办公网/跳板机 | allow + limit | SSH 管理口 |
| 80/tcp | 入站 | 0.0.0.0/0 | allow | Web |
| 443/tcp | 入站 | 0.0.0.0/0 | allow | Web |
| 9100/tcp | 入站 | 10.10.0.0/16 | allow | 监控拉取 |
| 8080/tcp | 入站 | 10.10.0.0/16 | allow | 内网 API |
| 任意 | 入站 | 任意 | deny(默认) | 最小开放 |
线上变更时,我会先在 B 会话持续 journalctl -u ssh -f,确认我自己的 SSH 没被限太狠,同时用 nmap -Pn <server> -p 22,80,443,9100 快速视察外显面。
5. Fail2ban:让“行为”说话,动态封
5.1 安装与总体策略
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
总体原则
- 后端用 systemd(Ubuntu 22.04 journald 记录更全):backend = systemd
- 封禁动作交给 UFW:banaction = ufw(Fail2ban 处理条件,UFW 负责落地规则)
- 分级封禁(首次短封,反复加码)
在 /etc/fail2ban/jail.local(或 jail.d/10-hardening.local)写入:
[DEFAULT]
# 依据实际情况把你的办公/跳板机网段加白
ignoreip = 127.0.0.1/8 ::1 203.0.113.10 198.51.100.0/24
banaction = ufw
backend = systemd
# 分级封禁策略(需要 0.11+)
bantime = 10m
findtime = 10m
maxretry = 4
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 12h
# 邮件或脚本告警可选(后面给示例)
destemail = ops@example.com
sender = fail2ban@server.example.com
mta = sendmail
[sshd]
enabled = true
port = 22
logpath = %(sshd_log)s
maxretry = 4
findtime = 10m
bantime = 30m
# 如果你的 SSH 端口改了:
# port = 2222
# Nginx 认证失败(如果有 HTTP Basic/Auth)
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
# 常见扫描/爆破:例如针对 wp-login、xmlrpc、路径探测等
[nginx-badbots]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
filter = nginx-badbots
maxretry = 2
findtime = 10m
bantime = 1h
# 多次“累犯”二次封(读取 fail2ban.log)
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = ufw
bantime = 24h
findtime = 24h
maxretry = 5
说明:nginx-badbots 过滤器在 /etc/fail2ban/filter.d/nginx-badbots.conf,如系统没有,可自定义。下面给一个“路径探测 + 常见恶意 UA”的简单示例。
创建自定义过滤器 /etc/fail2ban/filter.d/nginx-probing.conf:
[Definition]
failregex = ^<HOST> - .* "(GET|POST) /(wp-login\.php|xmlrpc\.php|\.git/|\.env|phpmyadmin/|vendor/|config/)" .* (404|401|403) .*
^<HOST> - .* "(GET|POST) /.*(admin|login|shell)\.php" .* (404|401|403) .*
ignoreregex =
并在 jail.local 增加:
[nginx-probing]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
filter = nginx-probing
maxretry = 2
findtime = 10m
bantime = 1h
5.2 UFW 与 Fail2ban 的“联动”如何落地
Fail2ban 命中后,会调用 action.d/ufw.conf,把攻击源 IP 加到 UFW 的拒绝规则里。你可以在日志里看到:
sudo tail -f /var/log/fail2ban.log
# ... Ban 203.0.113.200
sudo ufw status numbered | grep 203.0.113.200
解除封禁:
sudo fail2ban-client set sshd unbanip 203.0.113.200
# 或者(不推荐手改)用 ufw delete 指定规则序号
5.3 验证与压测
正向验证:从允许网段正常 SSH / HTTP 访问。
黑盒扫描(另一台测试机):
nmap -Pn <server> -p 22,80,443 --max-retries 1
模拟爆破(温和一点,别弄生产账户):
# 错误口令尝试几次,观察 /var/log/auth.log 与 fail2ban.log
ssh test@<server>
查看状态:
sudo fail2ban-client status
sudo fail2ban-client status sshd
6. 让监控与告警“有温度”
我在现场会把封禁事件抄送到邮件或 Webhook(例如企业微信/Slack)。一个轻量的做法是自定义 action。
新建 /etc/fail2ban/action.d/notify-webhook.conf:
[Definition]
actionban = curl -s -X POST -H "Content-Type: application/json" -d '{"host":"<fq-hostname>","jail":"<name>","ip":"<ip>","time":"<date>"}' https://ops.example.com/f2b
actionunban = /bin/true
在 jail.local 的 [DEFAULT] 里叠加:
action = %(action_)s
notify-webhook
生产里我会在 Webhook 服务端加签名校验与速率限制,防止“通知风暴”。
7. 运行日常:常用命令清单
UFW
sudo ufw status verbose
sudo ufw app list
sudo ufw allow from <CIDR> to any port <PORT> proto tcp
sudo ufw delete <rule-number>
Fail2ban
sudo fail2ban-client ping
sudo fail2ban-client status
sudo fail2ban-client status nginx-probing
sudo fail2ban-client set sshd banip <IP>
sudo fail2ban-client set sshd unbanip <IP>
日志
sudo tail -f /var/log/auth.log
sudo journalctl -u fail2ban -f
sudo tail -f /var/log/fail2ban.log
8. 典型“坑位”与我在现场的解法
锁死 SSH 的惊魂
现象:ufw enable 后 SSH 卡断。
复盘:未先放行跳板机网段。
处置:幸好有 at now + 5 minutes "ufw disable" 的保险。回连后补充白名单,再 ufw enable。
IPv6 忘了开
现象:IPv4 阻得住,IPv6 照样能打进来。
解决:/etc/default/ufw 里 IPV6=yes,并检查 ufw status 是否列出 v6 规则。
Nginx 日志格式不匹配
现象:Fail2ban nginx-probing 不触发。
解决:确认 access_log 的 log_format 与 filter 正则一致。必要时调整 failregex。
容器化服务“看不见”日志
现象:应用跑在容器里,日志进了 stdout,Fail2ban 抓不到。
解决:把 Nginx/应用日志落盘到宿主机(bind mount),或让 Fail2ban backend 用 journal 抓 systemd 里的容器日志(需配套)。
iptables/nftables 冲突误解
现象:担心 UFW(iptables 语义)与 nftables 冲突。
说明:Ubuntu 22.04 用 iptables-nft 兼容层,UFW/Fail2ban 正常可用。只要不强行切回 legacy,一般没问题。
“换端口=安全” 的错觉
经验:端口变更只能降噪,不能替代认证与风控;我在现场仍然把 SSH 放在 UFW 速率限制 + Fail2ban 的双重保护下。
9. 进阶:按业务划分“动静态”封禁
静态层(UFW 持久规则):白名单办公/监控、固定服务端口开放策略。
动态层(Fail2ban → UFW 临时规则):根据行为拉黑,随时间自动回收。
再犯惩戒(recidive):对重复作恶者加码封禁时长。
灰度与审计:把 Fail2ban 的封禁事件同步到审计系统,定期复核误杀率(比如爬虫误触发的比例)。
10. 一份可复制的“落地清单”(Checklist)
- 带外可用,二终端在线,at 回滚保险已就绪
- 系统补丁、SSH 基线、root 禁用远程密码、密钥登录
- UFW 默认拒绝入站,只开放业务与白名单;SSH 限流
- Fail2ban 安装启用:backend=systemd,banaction=ufw,分级封禁
- Nginx/应用过滤器按日志格式验证通过
- recidive 启用,监控与告警联通
- 验证:nmap 可见端口正确;模拟爆破被封;误杀率低
- 文档化规则与回滚步骤,交接与演练完成
11. 关键配置一览(可直接落盘)
/etc/fail2ban/jail.d/10-hardening.local
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.10 198.51.100.0/24
banaction = ufw
backend = systemd
bantime = 10m
findtime = 10m
maxretry = 4
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 12h
[sshd]
enabled = true
port = 22
maxretry = 4
findtime = 10m
bantime = 30m
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
[nginx-probing]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
filter = nginx-probing
maxretry = 2
findtime = 10m
bantime = 1h
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = ufw
bantime = 24h
findtime = 24h
maxretry = 5
/etc/fail2ban/filter.d/nginx-probing.conf
[Definition]
failregex = ^<HOST> - .* "(GET|POST) /(wp-login\.php|xmlrpc\.php|\.git/|\.env|phpmyadmin/|vendor/|config/)" .* (404|401|403) .*
^<HOST> - .* "(GET|POST) /.*(admin|login|shell)\.php" .* (404|401|403) .*
ignoreregex =
UFW 最小规则(示例)
ufw default deny incoming
ufw default allow outgoing
ufw allow from 203.0.113.10 to any port 22 proto tcp comment 'JumpHost'
ufw allow from 198.51.100.0/24 to any port 22 proto tcp comment 'OfficeNet'
ufw limit 22/tcp comment 'SSH rate limit'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw allow from 10.10.0.0/16 to any port 9100 proto tcp comment 'NodeExporter'
ufw enable
12. 从“那一夜”到“每一夜”
再后来,我把那晚的封禁日志导出来做了个小可视化。凌晨 3 点到 5 点,来自不同 ASN 的 SSH 爆破像心电图一样起伏。UFW 像一道稳固的堤坝,Fail2ban 则像自动开合的闸门,谁超流量、谁作恶,就临时关一关。
清晨 6 点,我把保暖外套搭在机柜门上,坐在地板上嚼着冷掉的饭团,检查最后一条规则的注释是否写清楚。安全从来不是一次性的“设置”,而是每天的“运营”。
如果你也在香港、在任何一个外网暴露的节点守夜,愿这套把式能让你少熬几次夜。下一次看到扫描峰值,我希望你也能像我一样,淡定地看着日志,轻轻合上笔记本,然后去喝一口热茶。
附:常见问答
Q:只用 UFW 不用 Fail2ban 行不行?
A:可以,但效果差很多。UFW 解决“静态面”,Fail2ban 补上“行为面”的动态封禁,二者互补。
Q:Fail2ban 会不会误伤?
A:用 ignoreip 白名单、适度的 maxretry 与 findtime,再加上 recidive(重点打击重犯),误伤可控。我会在上线初期密切观察 48 小时。
Q:更进一步要做什么?
A:加 WAF、对外 API 入口拉灰产情报、容器/内核加固(AppArmor/SELinux)、以及资产侧的暴露面收敛(端口降维)。