上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-08-21 10:12 阅读量:1375


那天凌晨 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)、以及资产侧的暴露面收敛(端口降维)。

目录结构
全文