为什么云服务器默认的防火墙配置一上线就等于没有防护
很多站长买完 VPS 的第一件事是装 Nginx、跑 WordPress,然后打开浏览器看着首页正常显示就认为万事大吉。可实际情况是:云厂商控制台的「安全组」里默认放行了 22、80、443,而系统内部的 ufw 或 firewalld 往往是关着的。这意味着你的机器只有一层防护,而且这一层是云平台替你代管的——一旦你换了机器、换了厂商,或者为了调试图省事把安全组改成了「全部放行」,整台机器就赤裸裸挂在公网上。
更麻烦的是,能扫到你的不只是人。从启动到被扫描,通常只需要十几分钟。SSH 爆破、Redis 未授权、MySQL 3306 暴露、.env 文件爬取,这些都是全自动的脚本,一天几百次尝试是常态。本文不讨论「装个防火墙软件就完事」这种表层说法,而是把 Linux 防火墙从规则模型、状态跟踪到四层/七层组合防御的路子完整走一遍,让个人站长在一台 1 核 2G 的机器上也能做出靠谱的边界防护。
先弄清你面对的是几层防护
一台云主机的网络防护实际上至少有三层,很多人只做了其中一层就以为安全了:
- 云平台安全组:在宿主机侧用虚拟交换机过滤,最外层,性能损耗为零,但逻辑简单——只有协议、端口、源 IP,看不懂应用层。
- 系统防火墙:ufw / firewalld / 直接写 nftables,在虚拟机内部生效。它的优势是能和系统服务、Docker 网络、日志联动。
- 应用层防护:Nginx 的 limit_req、fail2ban 的日志匹配、WAF 规则。这一层才真正看得懂「请求的内容」。
正确的做法是三层叠加而不是三选一。安全组负责兜底(只开必要端口),系统防火墙负责精细化(谁能访问哪个端口、是否限速),应用层负责识别恶意行为。下面逐个展开。
把安全组收成「白名单 + 三个端口」
安全组的原则只有一句话:默认拒绝,按需放行。入方向规则应该收敛成下面这样:
入方向
80/tcp 0.0.0.0/0 HTTP
443/tcp 0.0.0.0/0 HTTPS
22/tcp 你的固定出口IP/32 SSH(强烈建议)
出方向
全部放行(或仅放行 80/443/53/25 之外的常用端口)如果你没有固定 IP(家里宽带、手机热点),至少要把 22 端口改到一个不常用的高位端口,比如 2222、5822。改端口本身不增加真正的安全性,但它能把日志里 90% 的无脑爆破挡在门外,让你更容易发现真正盯着你的那部分流量。
还有一个高频踩坑:不要为了图省事在安全组里放行 ALL。见过太多人装 Docker 之后发现容器端口访问不了,就去把安全组改成「全部端口放行」,结果 MySQL 3306、Redis 6379 直接暴露在公网。正确做法是回来看 Docker 的 iptables 规则,而不是关掉安全组。
系统防火墙选型:ufw 还是 nftables
ufw:适合单机、命令少、易读易审计
Debian/Ubuntu 上 ufw 是 iptables 的一层友好封装,规则简单清晰,适合个人站。推荐的初始配置:
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw logging low
ufw enable
ufw status verbose要注意 ufw enable 之前必须先放行 22,否则你会把自己锁在门外。真的锁了也别慌,云厂商控制台的 VNC 或「救援模式」可以进去,这是唯一的后路,所以务必提前确认控制台能登录。
ufw 的一个隐藏陷阱是和 Docker 的兼容性。Docker 会直接在 DOCKER-USER 链和 nat 表里插入规则,而 ufw 的规则在 INPUT 链上。结果是:容器映射的端口会绕过 ufw。也就是说你 ufw deny 3306 之后,如果启动了 docker run -p 3306:3306 mysql,3306 依然是开放的。这个问题已经有无数人中招。
解决办法有两个,推荐第一个:
# 方案一:容器端口只绑本地回环,由 Nginx 反代对外
docker run -d -p 127.0.0.1:3306:3306 mysql:8.0
# 方案二:在 DOCKER-USER 链里做兜底限制
iptables -I DOCKER-USER -i eth0 ! -s 10.0.0.0/8 -j DROP方案一是从根上解决:容器永远不直接面对公网,所有对外流量都必须经过 Nginx。这样你只需要维护 Nginx 一处配置,审计也简单得多。
nftables:规则更少、性能更好
Debian 10+ 起 nftables 已经是默认后端,ufw 生成的规则底层也是 nft。如果你追求更清晰的规则集,可以直接写 /etc/nftables.conf:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set blackhole {
type ipv4_addr
flags timeout
}
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
ip saddr @blackhole drop
ip protocol icmp icmp type { echo-request } limit rate 4/second accept
ip6 nexthdr ipv6-icmp accept
tcp dport { 22, 80, 443 } ct state new accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}这里的 ct state established,related accept 是所有状态防火墙的核心:它让已经建立的连接(比如你的 SSH 会话)自动继续放行,不需要为出站流量单独写规则。忘记这一条会让「能连上但马上断」这种怪问题困扰你半天。
blackhole 集合是一个很实用的技巧:可以临时封禁 IP 而不改规则文件,支持超时自动解封。比如:
nft add element inet filter blackhole { 45.148.10.7 timeout 24h }这个写法可以配合 fail2ban 使用,或者写进自己的一键封禁脚本,后面会讲。
用 fail2ban 把日志变成封锁动作
防火墙只认 IP,不认行为。而日志里恰恰全是行为。fail2ban 的作用就是监控日志、匹配失败模式、调用防火墙封禁。它是个人站防守 SSH 爆破和 WordPress 登录爆破最划算的工具,资源占用几乎为零。
Debian 12 上安装并启用 SSH 防护:
apt install fail2ban -y
cat > /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
ignoreip = 127.0.0.1/8 ::1 你的家庭IP
[sshd]
enabled = true
port = ssh,2222
logpath = %(sshd_log)s
maxretry = 3
bantime = 24h
EOF
systemctl enable --now fail2ban
fail2ban-client status sshd几个关键点:
bantime太短意义不大。脚本是自动的,封 10 分钟基本等于没封。建议真实爆破保持 24 小时起步,甚至可以设成递增(bantime.increment = true,bantime.factor = 2)。ignoreip一定要把你的管理 IP 写进去。否则一次手滑输错密码 3 次,你自己就被关在门外,而 SSH 又被封,只能走控制台。backend = systemd在 Debian 12 上是必须的,因为 sshd 默认不再单独写/var/log/auth.log,日志进了 journald。老教程里让你去看 auth.log 会发现文件不存在。
给 Nginx 加一层登录爆破防护
WordPress 的 wp-login.php 是另一个重灾区。可以在 fail2ban 里加一个 jail,匹配 Nginx access log 里的 401/403:
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 6h
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
maxretry = 4
findtime = 5m
bantime = 12h自定义 filter /etc/fail2ban/filter.d/wordpress-login.conf:
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php HTTP/[^"]+"
ignoreregex =用 fail2ban-regex 先验证过滤器能不能命中历史日志,这一步非常关键,规则写错会导致「配了但从不封人」而这种错误非常隐蔽:
fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-login.conf输出里 Lines matched 大于 0 才说明规则有效。
Nginx 层的限速:在封 IP 之前先降噪
fail2ban 是事后封禁,Nginx 限速是事前压制。两者配合能显著减少无效流量。在 http 块里定义限速区:
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_perip:10m;在 location / 里引用:
limit_req zone=perip burst=20 nodelay;
limit_conn conn_perip 20;
limit_req_status 429;burst 是令牌桶的容量,允许瞬间突发;nodelay 表示突发请求立刻处理而不是排队等待——对正常用户更友好,但对攻击者的压制稍弱。如果是为了防采集,去掉 nodelay 让请求排队会更有效。
还有一个被忽视的细节:限速要用 $binary_remote_addr 而不是 $remote_addr。后者是字符串,每个不同 IP 都占用更多内存;前者是压成 4 字节的二进制,10m 的 zone 能装下约 16 万个 IP,足够个人站。
一个常见误区:只封 IPv4 忘了 IPv6
现在很多 VPS 默认分配了 IPv6 地址,而且不少服务默认双栈监听。如果你只写 ip saddr 规则,IPv6 侧完全是敞开的。在 nftables 里要用 table inet filter 这一张表同时管两种协议,或者分别写 ip 和 ip6 规则。使用 ufw 的话它默认会在 /etc/default/ufw 里通过 IPV6=yes 控制,确认这个值是 yes。
验证方法很直接,从外部扫一遍:
# 从另一台机器扫本机 IPv4 与 IPv6 全部端口
nmap -Pn -p- --min-rate 5000 你的IPv4
nmap -6 -Pn -p- --min-rate 5000 你的IPv6两个结果应该完全一致,并且只出现你主动开放的端口。不一致就说明有一侧漏了。
验证防火墙到底生效没有
配置完不要只看 ufw status 显示 active 就放心,那个只反映配置文件状态。真正要验证的是实际连通性,建议从外部机器执行:
# 应该超时的端口(说明被拦住了)
nc -zv -w3 你的IP 3306
nc -zv -w3 你的IP 6379
# 应该成功的端口
curl -I -m5 https://www.你的域名.com同时看本机内核统计确认包被丢在哪一层:
nft list ruleset
conntrack -L | wc -l
ss -lntupss -lntup 是排查最关键的一步:它会告诉你每个端口监听在 0.0.0.0、127.0.0.1 还是 [::]。很多「防火墙明明拦了但还能访问」的问题,根因就是服务监听在 IPv6 的 [::] 上而防火墙上只写了 IPv4 规则。
把封禁做成日常运维动作
防火墙不是配完就结束的东西。建议每周花五分钟看一次日志,把反复出现的 IP 手动加进 blackhole 集合,把 fail2ban 的封禁统计拉出来看一眼:
# 过去 7 天被封次数最多的 IP
journalctl -u fail2ban --since "7 days ago" | grep "Ban " | \
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort | uniq -c | sort -rn | head -20如果发现某个 IP 段持续攻击,直接整段封掉比逐个封更省事:
nft add element inet filter blackhole { 45.148.10.0/24 timeout 7d }这些临时规则重启后会消失,这是好事——它们不该永久写进配置。如果你确实需要长期封禁,再写进 /etc/nftables.conf 并 systemctl reload nftables。
小结:个人站的防火墙最小可用组合
把上面的内容压缩成一份可以直接照做的清单:
- 云安全组:默认拒绝,只开 22(限你的 IP)、80、443。
- ufw 或 nftables:默认 drop 入站,放行 established 连接与上述三个端口。
- 服务监听地址:数据库、Redis、内部 API 一律绑
127.0.0.1,不绑0.0.0.0。 - Docker 端口:全部
-p 127.0.0.1:x:y,对外统一走 Nginx。 - fail2ban:sshd + nginx 登录页两条 jail,bantime 不少于 12 小时,ignoreip 写自己。
- Nginx:limit_req + limit_conn 按 IP 限速,状态码 429。
- 每周一次外部端口扫描验证:IPv4 和 IPv6 都要扫,结果必须一致。
这套配置在一台 1 核 1G 的机器上额外开销几乎为零,但它能挡掉绝大多数的自动化攻击。真正决定服务器能否长期稳定运行的,往往不是多高级的架构,而是这些基础边界有没有认真守住。防火墙配好之后,你会发现日志干净了,CPU 毛刺少了,出问题时也更容易一眼定位到真正的原因。