半夜被告警电话叫醒,登上一台 Linux 服务器,发现系统负载高得离谱,翻日志一看,SSH 端口上全是来自陌生 IP 的暴力破解尝试。这时候我才意识到,光是把 FirewallD 装上、放行几个端口,根本不算“配好防火墙”,更谈不上“安全”。真正重要的是两件事:持续监控它,以及按照最小暴露原则把它加固到位。
这篇内容就是围绕这两件事展开的,目标很明确:面向 CentOS Stream / RHEL / Rocky Linux / Fedora 这类 systemd + FirewallD 体系的 Linux 服务器。无论你是刚接手服务器的运维新手,还是已经在用 FirewallD 但总觉得哪里不对劲的开发者,都能在这里找到一套可以直接照抄的方案。我不会绕弯子,直接把配置、命令、踩坑记录都摊开写给你。
1. 先看懂 FirewallD 的三层模型:区域、服务、富规则
1.1 为什么“默认区域”能决定你服务器的生死
很多人在配置 FirewallD 时,第一步就错了——他们从来不关心当前默认区域是什么,只会看到端口不通就临时--add-port。但 FirewallD 是一条“默认拒绝链路”还是“默认放行链路”,完全取决于默认区域。
默认区域就是这台服务器上所有没有明确指定归属的流量的处理策略。FirewallD 一共有 9 个预置区域,从trusted到drop,信任度依次递减。生产服务器上最常用的两个是:
public:默认拒绝外来连接,只有明确放行的服务和端口才允许进入,适合面向公网的服务器。drop:所有外来连接直接丢弃,连拒绝回包都不给,适合暴露在公网但不提供任何服务的节点。
我见过不少服务器,网卡被 NetworkManager 自动绑定到了home或者work区域,这两个区域默认信任程度比public高得多,很多端口没放行却也能通。结果管理员以为防火墙在工作,实际等于透明。所以拿到一台新机器,先执行:
firewall-cmd --get-default-zone firewall-cmd --get-active-zones第一个命令看默认区域,第二个命令看当前有哪些网卡绑定在哪个区域上。如果网卡不在你期望的区域,用下面的命令把网卡挪过来:
firewall-cmd --zone=public --change-interface=eth0 firewall-cmd --set-default-zone=public注意--set-default-zone会直接写入永久配置,不需要再加--permanent。而--change-interface会同时改变运行时和永久配置,实测最省事。做完这一步,才算有了一个“默认拒绝”的安全起点。
1.2 分清“运行时规则”和“永久配置”:这是 90% 配置不生效的根源
FirewallD 有一个特别容易坑人的设计:规则分两层——运行时层和永久层。
- 运行时层:当前正在生效的规则,存在于内存中,重启 firewalld 服务后就没了。
- 永久层:存在
/etc/firewalld/下的 XML 配置里,每次服务启动时重新加载。
不加--permanent的命令只改运行时层。我见过有人为了防止 SSH 被爆破,敲了一条firewall-cmd --add-rich-rule='rule source ip=203.0.113.7 drop',当时看着有效,结果一重启服务器,IP 照样能连接,因为那条规则压根没写进磁盘。
反过来也常见:加了--permanent添加端口,但忘了firewall-cmd --reload,于是“永久配置里有,运行时没有”,别人访问不了服务,你还找半天原因。
我的习惯是:每次查规则都同时看两层,对比差异:
firewall-cmd --list-all --zone=public firewall-cmd --permanent --list-all --zone=public如果两者不一致,用firewall-cmd --runtime-to-permanent把当前运行时规则保存为永久配置,或者用--reload让永久配置覆盖运行时。具体用哪个,取决于你想保留哪一层。刚配完规则想立即生效,就是firewall-cmd --reload。
1.3 服务、端口、富规则到底怎么选
FirewallD 放行流量有三种常用方式,新手最容易混淆:
| 方式 | 示例 | 适用场景 |
|---|---|---|
| 服务(service) | firewall-cmd --add-service=http | 该服务端口固定、语义明确,如 HTTP、HTTPS、SSH |
| 端口(port) | firewall-cmd --add-port=8080/tcp | 没有现成服务定义的应用端口 |
| 富规则(rich rule) | firewall-cmd --add-rich-rule='rule source address=... service name="ssh" accept' | 需要限定来源 IP、协议、端口组合,或多个条件的复杂放行 |
能用服务尽量用服务,因为服务定义里不仅有端口,还包含了协议等元信息。端口规则只写了tcp/udp和端口号,表达力弱一些。富规则是最灵活的,但不要滥用——它是 XML 结构,写多了自己都难维护。
我比较推荐的原则是:端口规则用于临时排查,服务规则用于长期放行,富规则只用于“带条件的放行或拒绝”。比如想放行某个内网网段访问 SSH,我会用富规则明确写出来,而不是把 SSH 端口向全世界开放。
2. 可照抄的 FirewallD 安全加固流程:从默认 zone 到 SSH 和 fail2ban
2.1 第一步:清点当前放行的服务和端口
加固之前,先给防火墙做一次“人口普查”。大多数发行版安装完 FirewallD 后,public区域里已经默认放行了一些服务,比如 SSH、DHCPv6 客户端,某些带图形化管理面板的发行版可能还开着 Web 管理端口。
执行:
firewall-cmd --zone=public --list-services firewall-cmd --zone=public --list-ports firewall-cmd --zone=public --list-rich-rules看到的结果可能比你预期的多。我处理过的服务器里,最常见的问题是“历史遗留”:某次调试时添加了一个端口规则,后来服务下线了,规则却一直留在那儿。这些遗留规则就是攻击面,要一条条确认并清理。不确定某个服务还有没有用,我的判断标准是:这个端口在公网上有没有对应的业务进程监听,用ss -tlnp看看监听端口,再对照防火墙放行列表,能对上号的才保留。
移除不需要的放行规则:
firewall-cmd --permanent --zone=public --remove-service=service-name firewall-cmd --permanent --zone=public --remove-port=8080/tcp firewall-cmd --reload2.2 加固 SSH:换端口、限来源、禁 root 直登
SSH 是每台 Linux 服务器都必然开着的服务,也是最容易被爆破的目标。就算防火墙默认拒绝了外来端口,22 端口只要开着,扫描器就一定会来试。我的建议分三步:
第一步:修改 SSH 监听端口。编辑/etc/ssh/sshd_config,把#Port 22改成Port 2222之类的非标准端口。改完之后要在防火墙里同步放行新端口:
firewall-cmd --permanent --zone=public --add-port=2222/tcp firewall-cmd --reload这里有个容易被忽略的坑:在 RHEL 系发行版上,SELinux 默认只允许 SSH 监听 22 端口,如果你直接改端口,SSH 服务可能起不来。需要把新端口加进 SELinux 的 SSH 端口类型:
semanage port -a -t ssh_port_t -p tcp 2222第二步:限制 SSH 来源 IP。如果办公 IP 固定,最好直接用富规则限定来源,而不是对全网放行:
firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port port="2222" protocol="tcp" accept' firewall-cmd --reload注意,加了这条放行规则后,原本public区域里对 SSH 服务的放行要记得移除,否则限制来源就失去了意义。
第三步:禁止 root 直接登录。在sshd_config里设置PermitRootLogin no,日常用普通用户登录后再sudo su -切换。这是性价比极高的防线,能在源头上杜绝“root 弱口令被爆破”这类事故。
2.3 用 fail2ban 联动 FirewallD 做自动化封禁
只靠防火墙拦 IP 登录,终究是静态的。攻击源 IP 频繁更换,你得不停手动添加拒绝规则。这时候就该上 fail2ban 了。它监控日志里的登录失败记录,达到阈值后调用 FirewallD 动态封禁来源 IP。
在 RHEL 系发行版上,fail2ban 的配置文件是/etc/fail2ban/jail.local,可以参考以下内容:
[DEFAULT] ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 banaction = firewalld maxretry = 5 findtime = 600 bantime = 3600 [sshd] enabled = true filter = sshd backend = systemd action = firewalld[port="2222", protocol="tcp"]这里的action = firewalld[...]会要求 fail2ban 调用firewall-cmd --add-rich-rule,给触发告警的 IP 生成一条临时拒绝富规则。bantime是封禁时长,单位秒,我一般先设 1 小时观察效果,确认无误后再调长。
fail2ban 启动后,可以用fail2ban-client status sshd查看当前被封禁的 IP 列表。有个细节提醒一下:fail2ban 的backend = systemd直接读取 journald 日志,比监控/var/log/secure文件更不容易漏数据,前提是 SSH 服务完整走 audit 日志输出,所以建议优先用 systemd 后端。
2.4 别忽视 ICMP 和协议层面的策略
很多人配置防火墙只看 TCP/UDP 端口,完全忽略 ICMP(ping、traceroute 等)。我不建议把所有 ICMP 都禁止,因为网络连通性排查也依赖它。但有些类型确实该关,最典型的是ICMP redirect(重定向),它可能被用来操纵路由表,属于一种协议层面的攻击面。
关闭 ICMP redirect:
firewall-cmd --permanent --zone=public --add-icmp-block=redirect firewall-cmd --reload同时可以用firewall-cmd --permanent --zone=public --remove-icmp-block=echo-request之类的方式按需保留 ping。这里的核心原则还是“最小化”,不是把所有协议都理解成洪水猛兽,而是只保留业务和运维必需的。
3. 实时监控体系:状态、日志、告警三步走
3.1 一条命令快速掌握防火墙当前状态
安全加固做完了,接下来要让监控变成日常习惯。我的日常巡检命令是这些:
systemctl status firewalld --no-pager firewall-cmd --state firewall-cmd --get-active-zones firewall-cmd --list-all --zone=public第一条看服务是否活着,第二条看防火墙运行状态,第三条看接口归属,第四条看详细规则。这几条命令组合在一起,10 秒内就能判断一台服务器的防火墙是否处于预期状态。
如果用的是新版本 FirewallD(底层基于 nftables),还可以用nft list ruleset直接看内核里的完整规则链。注意,这里的规则链和iptables -L不是一回事,新版本下iptables命令看到的可能只是兼容层内容,容易误判,排查时优先用nft list ruleset。
3.2 开启拒绝日志:让扫描行为无处遁形
FirewallD 有一个很实用但很多人不知道的参数——--set-log-denied=all。开启后,所有被防火墙拒绝的数据包都会记录到系统内核日志中,你可以清楚地看到谁在扫描你的服务器,访问了哪些端口。
firewall-cmd --permanent --set-log-denied=all firewall-cmd --reload查看拒绝日志:
journalctl -k --since today | grep -E 'DROP|REJECT'日志会显示来源 IP、目标端口和协议,比如这样的形态(不同发行版格式略有差异):
kernel: DROP IN=eth0 OUT= MAC=... SRC=203.0.113.10 DST=198.51.100.2 LEN=40 PROTO=TCP SPT=40000 DPT=23看到大量来自同一 IP 对 23、3389、3306 等端口的请求,基本可以判断是扫描器在试探,可以直接用富规则封掉。
不过这个日志开关有个副作用:流量一大,内核日志会被刷爆。我的建议是,先开一段时间摸清扫描规律,确认哪些 IP 该封,哪些端口最常被试探,再考虑是否永久开着。长期开的话,务必配合日志轮转和保留策略一起做。
3.3 规则漂移监控:用脚本盯住“配置被偷偷改掉”
比端口被扫描更危险的情况是防火墙配置不知不觉被改了。可能是运维同事临时排障后忘了恢复,也可能是自动化脚本误操作。我习惯在每台服务器上放一个规则快照脚本,通过 cron 定时比对规则是否发生漂移。
脚本思路很简单:把当前生效规则的哈希存下来,下次执行时重新计算,不一致就发告警。可以用以下脚本做基础版本:
#!/usr/bin/env bash STATE_FILE=/root/.firewalld_state.sha256 SERVICE=firewalld # 检查服务是否存活 if ! systemctl is-active --quiet "$SERVICE"; then echo "firewalld 未运行" | mail -s "防火墙宕机告警" admin@example.com fi # 检查规则是否漂移 firewall-cmd --list-all --zone=public > /tmp/firewalld_now.txt sha256sum /tmp/firewalld_now.txt > /tmp/firewalld_now.sha256 if [ -f "$STATE_FILE" ]; then if ! cmp -s "$STATE_FILE" /tmp/firewalld_now.sha256; then echo "防火墙规则发生变化,请登录确认" | mail -s "规则漂移告警" admin@example.com fi fi cp /tmp/firewalld_now.sha256 "$STATE_FILE"配合 cron 每小时执行一次,基本上配置被改掉的 1 小时内就能发现。告警方式可以用mail,也可以改成调用企业微信、飞书、Slack 之类 Webhook,只要你本地有对应的工具就行。
3.4 日志量太大怎么办:轮转、过滤与采样
拒绝日志开启之后,最现实的问题就是日志增长太快。journald 默认会把内核日志和系统日志一起管理,不设上限会持续积累到磁盘满。
处理方案有两个:
第一,限制 journald 占用的空间。编辑/etc/systemd/journald.conf,设置:
SystemMaxUse=500M MaxRetentionSec=7day改完执行systemctl restart systemd-journald生效。这样日志最多占用 500MB,超过 7 天的自动丢弃。
第二,过滤出真正有价值的告警。不要直接对着journalctl -k的滚动日志肉眼看,而是用条件筛选:
journalctl -k --since "1 hour ago" | grep DROP | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20这条命令统计最近一小时来自不同端口的拒绝次数,一眼就能看到哪些端口正在被高频试探。把这类查询做成每周定时邮件报告,比天天盯终端轻松得多,也不容易漏。
4. 那些年踩过的坑:Docker 冲突、IPv6 盲区和 reload 陷阱
4.1 Docker 与 FirewallD:为什么容器端口映射时好时坏
服务器上跑着 Docker 的话,FirewallD 和 Docker 的规则管理机制很容易打起来。Docker 默认会操作内核 iptables 链来创建端口映射的 DNAT 规则,而新版 FirewallD 底层用的是 nftables。两套体系在规则链的处理上存在冲突的可能,最典型的现象是:防火墙执行了某些操作之后,容器端口映射失效,外部访问宿主机的 8080 端口指向的服务不通了。
出现这种情况,第一反应不是去动防火墙规则文件,而是先检查 Docker 服务状态和规则链:
systemctl status docker --no-pager ss -tlnp | grep 8080如果宿主机监听没问题,但外部访问不通,大概率是 Docker 的 NAT 规则被防火墙重载影响了。最简单的恢复手段是重启 Docker 服务:
systemctl restart dockerDocker 重启后会把端口映射的规则重新写入内核,一般能恢复。但从根本上讲,生产环境里我建议:在需要调整防火墙规则时,优先用firewall-cmd --reload,不要轻易用systemctl restart firewalld或--complete-reload,后者清掉动态规则的概率大得多。如果 Docker 和 FirewallD 的冲突持续存在,可以评估是否调整 Docker 的网络模式,或者把 Docker 容器迁移到独立网段,但这类改动要在维护窗口测试,不要直接在业务高峰期操作。
4.2 IPv6 是防火墙的盲区:扫描一遍再下结论
很多运维配置 FirewallD 的时候,潜意识里只关心 IPv4。但 FirewallD 的规则默认同时作用于 IPv4 和 IPv6,也就是说,你给public区域放行的端口,IPv6 流量一样能进。
这里有个致命盲区:如果服务器有 IPv6 地址,但应用服务只监听在 IPv6 地址上,而你的防火墙规则恰好默认放行了某个服务,攻击者可能绕过 IPv4 防线,从 IPv6 通道直接访问服务。我在一次内部审计中就遇到过:某服务只绑了 IPv6 socket,运维人员完全没意识到防火墙规则对 IPv6 同样生效,结果服务毫无防护地暴露在公网。
检查思路很简单:
ip -6 addr show ping6 -c 2 2001:db8::1确认服务器确实启用了 IPv6,再去firewall-cmd --list-all里检查,凡是明确只允许 IPv4 访问的规则,必须在富规则里加上family="ipv4"限定。如果服务器不需要对外提供 IPv6 服务,可以考虑在系统层面关闭 IPv6 或至少把默认区域规则改成只放行必要的 ICMPv6 类型。这里要特别提醒,不要轻易禁止所有 ICMPv6,IPv6 邻居发现等功能依赖它,禁止过头会导致 IPv6 网络异常。
4.3 reload 了为什么还不对:运行时、永久与未保存的规则
“我明明加了规则,也 reload 了,为什么还是不生效?”这类问题的排查思路,九成落在运行时与永久配置的差异上。
举一个实际案例:某开发者在public区域添加了 8080 端口的永久放行,然后立即执行firewall-cmd --reload,本地访问 8080 却不通。后来查出原因,问题根本不在防火墙,而是后端服务还没起来,或者服务监听在127.0.0.1而不是0.0.0.0。防火墙的规则只是放行到端口的数据包,如果服务本身没监听,外面的包根本没有接收者。
另一个常见原因是:你添加的规则作用区不对。用--zone=internal加了放行,但网卡还在public,那规则自然不生效。排查时先确认网卡和区域的关系,再看服务监听状态,然后看防火墙放行列表,这个顺序基本能覆盖绝大多数“不生效”的场景。
4.4 complete-reload 不是你想用就能用
firewall-cmd --reload和firewall-cmd --complete-reload只差一个词,后果天差地别。
--reload会重新加载永久配置,但不会彻底重启防火墙服务,已建立的连接通常不会中断。而--complete-reload会完全停止并启动 firewalld,期间所有基于用户态规则管理的动态规则都会清空重建,已建立的连接也会断开。
在生产服务器上,--complete-reload基本等于一次有损操作。Docker 的动态映射、fail2ban 加进去的临时封禁规则,都会在这次操作中丢得一干二净。执行完你还要靠其他手段把这些规则找回来,反而制造了更多的管理噪音。
我的原则是:日常配置变更一律--reload;确认需要彻底重置防火墙内核链的时候,再在维护窗口执行--complete-reload。如果你连自己为什么会走到--complete-reload这一步都想不清楚,那大概率不需要它。
最后再分享一个我个人的操作习惯:每次修改 FirewallD 配置之前,我都会先做一份备份,把/etc/firewalld/目录完整打包扔到/root/下,文件名带上日期。这样不管发生了什么意外,都能在两分钟内退回上一个状态。另外,大规模变更前先在测试环境完整跑一遍操作流程,验证规则顺序和 Docker 兼容性,这比事后抢救有用得多。FirewallD 本身不复杂,复杂的是你所在的服务器环境,保持敬畏,保持备份习惯,就能少踩很多坑。