1. 为什么我劝你从iptables换到firewalld:三个颠覆认知的设计
先聊个真实场景。你在一台CentOS服务器上部署了一个Web服务,端口8080,配置完一切正常。结果服务器一重启,服务起不来了,排查半天发现是防火墙规则丢了。你在iptables里辛辛苦苦写的规则,有个别儿必须在restore之后重新加载,或者写进了错误的链里,重启之后直接被打回原形。这个经历我相信每个Linux运维都遇到过。
firewalld走进视野的时候,我的第一反应是:又一个包装iptables的工具,没必要学。但真正用了两个项目之后,我得承认自己的判断错了。firewalld不是一个简单的命令封装,它从设计层面改变了防火墙的配置方式,把"面向规则的配置"变成了"面向区域的配置",把"一次性覆盖式加载"变成了"运行时与永久配置分离"。
这套设计的直接收益是什么?你不需要再关心iptables那些链的顺序问题,不需要担心重启丢配置,不需要在脑子里维护一张"规则表"。你需要做的只是回答一个问题:这台服务器要提供什么服务,给谁提供服务。剩下的,firewalld帮你翻译成iptables规则。
如果你是第一次接触防火墙配置,或者被iptables那套庞大的规则体系劝退过,firewalld可能是最适合你的起点。它虽然抽象了一层,但这层抽象恰好把防火墙最常见的操作场景(开放端口、放行服务、限制来源IP)做成了"看见即所得"的配置项。这篇文章我会从zone机制讲到富规则,从docker端口映射冲突讲到排查链路,每个部分都是实际项目里碰过壁之后总结出来的经验。
2. zone机制拆解:为什么说"区域"比"链"更适合人类理解
2.1 九个内置zone的行为差异
firewalld默认带了九个zone:drop、block、public、external、dmz、work、home、internal、trusted。它们的信任级别从低到高排列:drop进入就丢弃,trusted放行所有流量。
这份默认清单,本质上是对服务器所处网络环境的预分类。生产环境最常见的组合是:对外服务的机器用public,办公网内部的机器用internal或者trusted,核心数据库单独划一个zone而且默认drop。不要在服务器上搞一堆花哨的zone,简单粗暴反而好维护。
我记得第一次配置zone的时候,把一台数据库服务器划到了trusted,理由是"反正内网都是可信的"。后来安全审计的时候被问到这个问题,我才意识到:trusted意味着任何来源、任何端口都能访问这台数据库。正确做法应该是建一个db专属zone,只放行3306端口,而且来源IP限定在应用服务器网段。
2.2 网卡与zone的绑定关系:配置不生效的头号根源
zone与网卡的绑定关系,是新手最常踩的坑,也是很多"我明明开放了端口为什么还连不上"问题的根源。
firewalld判断一个连接属于哪个zone,依据的是数据包的来源网卡。服务器上有两块网卡,eth0连接外网,eth1连接内网。你把eth0绑到了public,把eth1绑到了internal。如果你只改了public的规则,eth1那边用internal的规则管着,两边表现就不一样。
检查当前网卡绑定的zone,用这个命令:
firewall-cmd --get-active-zones输出像这样:
public interfaces: eth0 internal interfaces: eth1看到没有,每块网卡归属哪个zone一目了然。如果你在public里放行了8080端口,但你的客户端流量实际上从eth1进来,命中的是internal规则,那你放开等于没放。
还有一种情况需要特别留意:firewalld允许使用源地址规则。也就是说不光网卡可以决定zone,来源IP地址也可以。比如你有一条规则"192.168.1.0/24走internal",那么即使这块网卡绑的是public,来自这个网段的请求也会被分发到internal处理。
2.3 自定义zone的实战场景
内置zone不够用或者语义不清晰的时候,自定义zone是标准解法。
举一个我曾经处理过的场景:一台服务器上有多个服务,A服务面向公网,B服务只给公司内部员工用,C服务是后台管理端口。三个服务的流量都从同一块网卡进来,你没法用"网卡绑zone"来区分,只能用源IP细分。
自定义zone的操作:
firewall-cmd --permanent --new-zone=mgt firewall-cmd --permanent --zone=mgt --add-service=ssh firewall-cmd --permanent --zone=mgt --add-source=203.0.113.0/24 firewall-cmd --reload这个mgt zone的含义是:来自203.0.113.0/24这个网段的流量,走mgt的规则,允许SSH访问。其他来源不受影响。
用zone的最大好处是,你的规则集合是和网络区域绑定的,一段时间之后回看配置,能立刻理解"谁在什么场景下能用什么端口",而不是面对一长串按时间顺序堆叠的iptables规则逐行分析。
3. 运行时配置与永久配置:双轨制背后的设计逻辑
3.1 为什么设计成两条轨道
firewalld把配置分成两个层次:runtime(运行时)和permanent(永久)。你在命令行敲一条规则,默认只作用在运行时,不落盘。要写进配置文件让它重启之后还在,必须加上--permanent参数。
一开始我觉得这个设计很啰嗦,为什么不直接改配置文件然后reload?直到我线上环境踩过一次坑才明白,这两个层次的分离,本质上是"热变更"和"持久化"的分离。
想象一个场景:生产数据库正在跑业务,你想调整防火墙规则。如果改完配置立刻生效,万一规则写错把现有连接全断了,业务直接挂掉。如果是runtime模式,恢复原状只需要firewall-cmd --reload或者--remove-port;如果是permanent模式,你得先改文件再reload,多一步操作,多一次出错的机会。
所以我的习惯是:上线新服务的时候,先用runtime模式测试规则,确认业务正常,再补上--permanent把配置固定下来。等下次reload的时候,runtime和permanent合并生效,规则永续。
3.2 reload到底做了什么
很多人的认知里,reload是把配置文件重新读一遍。听起来没错,但有一个容易被忽略的细节:reload之后,所有runtime期间的临时规则会被清空,一切回到permanent配置。
这个机制踩过坑的都知道疼。有一次我在测试环境里临时放行了一个端口,过了几个小时发现端口不通了,排查了半天才想起来中间有人执行过reload或者firewalld服务重启了。
在运维脚本里如果有类似的批量操作,建议在脚本关键位置加一次--get-runtime-permanent状态的对比,心里有数。或者干脆把临时规则也写成permanent模式,测试完再删,避免被reload误伤。
3.3 双轨制的常见误区
整理一下我见过的高频误区:
- 只加了--permanent,没有reload,规则没生效。很多人以为permanent就是立即生效,实际上--permanent只是写入了配置文件,要等reload或服务重启才会真正加载。
- 只改了runtime,忘了permanent,服务器重启规则消失。
- reload之后之前的runtime规则全部丢失,对"临时放行"这个操作的理解不准确,把临时当成了持久。
这里我自己有一个小技巧。调试阶段可以先开一个"runtime变化记录模式",每次调整规则后,用--list-all检查当前zone的完整配置,同时把permanent的配置也拉出来对比一下:
firewall-cmd --zone=public --list-all firewall-cmd --zone=public --permanent --list-all对比两份输出,就能立即看出哪些规则只停留在runtime层面,哪些已经落盘持久化了。这个检查习惯养成了,双轨制相关的坑基本能避掉八成。
4. 端口、服务、富规则:三种粒度从粗到细的配置实战
4.1 服务配置:最省心但依赖定义文件
firewalld允许直接按服务名放行流量,不需要记端口。系统预置了SSH、HTTP、HTTPS、MySQL等常用服务定义。
firewall-cmd --zone=public --add-service=http firewall-cmd --zone=public --add-service=https默认服务定义文件放在/usr/lib/firewalld/services/里,自定义的放在/etc/firewalld/services/,格式是XML。以MySQL为例,默认的3306端口定义是这样的:
<?xml version="1.0" encoding="utf-8"?> <service> <short>MySQL</short> <description>MySQL Database Server</description> <port protocol="tcp" port="3306"/> </service>如果你用的服务端口不是标准端口,比如公司的MySQL跑在3307,你可以复制一份定义到/etc/firewalld/services/,修改port字段,然后按服务名放行。这种方式的好处是配置语义化:看到--add-service=kafka就知道放行的是Kafka端口,而不是一个意义不明的31442。
4.2 端口配置:最常用的粗粒度放行
端口配置是实际运维中用得最频繁的:
firewall-cmd --zone=public --add-port=8080/tcp firewall-cmd --zone=public --add-port=1000-2000/udp支持tcp、udp两种协议,也支持端口段。这里有一个容易忽略的细节:添加端口时协议必须写清楚,很多新手漏掉/tcp或/udp,命令报错后又回过头来找原因。
多个端口同时添加时,可以使用端口组:
firewall-cmd --zone=public --permanent --add-rich-rule='rule family="ipv4" port port="8080-8090" protocol="tcp" accept'一次性放行连续端口段,配合富规则写法,比一条条add-port效率高得多。
用端口还是用服务,我的建议是:能用服务语义就用服务,服务定义可以集中管理端口对应关系,后面改端口只改文件,不用重新敲命令。但是如果你只是临时跑个测试服务,直接加端口更直接。
4.3 富规则:精确匹配来源IP、端口和协议
富规则(rich rule)是firewalld里表达能力最强的方式,适合做精细化控制。
一个基础的富规则示例:
firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.100" port protocol="tcp" port="22" accept'这表示允许192.168.1.100这个IP访问本机的22端口。
富规则还支持动作组合。比如拒绝特定IP访问Web服务,并记录日志:
firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.200" port protocol="tcp" port="80" log prefix="BLOCK_WEB" level="warning" reject'这里的log会把匹配到的流量打到系统日志,并打上BLOCK_WEB的标签,方便后面在journalctl里搜索日志。生产环境排查的时候,这一条规则能帮你省掉很多抓包分析的力气。
再来一个更复杂的场景:公司内部有两段办公网段,192.168.10.0/24和192.168.20.0/24,客户要求只有市场部所在网段可以访问OA系统,而运维网段可以访问所有服务器。这就得靠富规则叠加了:
firewall-cmd --permanent --zone=internal --add-rich-rule='rule family="ipv4" source address="192.168.10.0/24" port protocol="tcp" port="8080" accept' firewall-cmd --permanent --zone=internal --add-rich-rule='rule family="ipv4" source address="192.168.20.0/24" port protocol="tcp" port="1-65535" accept' firewall-cmd --reload注意富规则之间是有顺序的,firewalld按规则添加顺序依次匹配,匹配到即停止。所以更精确的规则要放在前面,更宽泛的放在后面,否则可能出现某些IP被提前匹配到一个错误动作的情况。
4.4 三种方式怎么选
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 放行标准服务(HTTP、HTTPS、SSH) | 服务配置 | 语义化清晰,集中管理端口映射 |
| 临时调试放行某个端口 | 端口配置 | 操作最快,一眼看出放行了什么端口 |
| 限制特定IP访问特定端口 | 富规则 | 支持source地址、动作、日志的组合表达 |
| 复杂安全策略(多来源多动作) | 富规则 | 灵活度最高,可以处理绝大多数精细化需求 |
实际项目里这三者经常混用。常规服务用服务配置固定下来,临时调试用端口配置,涉及客户安全要求的部分用富规则实现并配日志。
5. docker端口映射与firewalld的冲突:绕不开的实战问题
5.1 问题现象
Docker和firewalld的冲突,是云服务器和容器化项目里高频出现的问题。
你运行了一个容器,把宿主机8080端口映射到容器内的80端口:
docker run -d -p 8080:80 nginx然后在firewalld里放行了8080端口:
firewall-cmd --zone=public --add-port=8080/tcp浏览器访问宿主机IP:8080,不通。但你在宿主机本地curl,服务正常。
5.2 冲突的本质原因
问题出在Docker操作iptables的方式上。Docker启动时会直接修改iptables规则,把自己的docker0网桥流量和端口映射写进DOCKER链。firewalld管理的是另一套链。两者互不感知。
有些版本和配置下,Docker的规则在FORWARD链上,而firewalld的规则也在FORWARD链上做处理。当Docker的链顺序在firewalld的策略链之前或之后,匹配结果可能完全不同,这就是"放行了却不通"或者"通了却没走过防火墙"的诡异现象。
更麻烦的是,有些环境下firewalld会把Docker的链清掉。比如你执行firewall-cmd --reload的时候,firewalld重新生成iptables规则,Docker链里的规则直接消失,所有端口映射全部失效。这也是为什么大家常说"重启firewalld之后docker端口映射不见了"。
5.3 处理方案对比
方案一:完全绕过firewalld管docker的流量。在/etc/firewalld/firewalld.conf里加:
FirewallBackend=nftables然后重启firewalld和docker。这种方式在部分环境下有效,但依赖内核版本和具体的发行版实现,不是万金油。
方案二:把docker的端口映射直接绑到宿主机网卡,让流量以NAT方式进入docker。这种场景下,你要在firewalld里放行的端口实际上是Docker映射端口对应的宿主端口,换句话说,就是8080/tcp。理论上配置正确就能通,但实操里受到docker0网桥的FORWARD策略影响,不一定每个版本都乖乖听话。
方案三:直接把docker使用的zone设为trusted。这是我认为最符合实际运维习惯的做法:
firewall-cmd --permanent --zone=trusted --change-interface=docker0 firewall-cmd --reload前提是你的docker网桥固定使用docker0,且你能明确docker流量的可信程度。把所有docker流量视为可信,意味着宿主机上的端口映射不再受firewalld管控,这适合内网开发和测试环境。生产环境把docker流量全部放行,需要评估安全风险。
从我的项目经验来说,如果你用的是Docker Compose管理多组服务,而且宿主机上还有其他生产服务共用同一块网卡,我不建议用trusted一刀切。更可靠的做法是控制docker容器启动参数的端口映射范围,并配合firewalld富规则对来源IP做限制,而不是让docker的流量完全旁路防火墙。
6. 配置不生效的完整排查链路:从现象到根因
我见过太多同事在防火墙配置上卡壳,最后绕来绕去发现不是因为firewalld,而是因为其他环节。这节我把完整的排查思路写下来,给你当参考。
6.1 第一步:确认firewalld状态和默认zone
systemctl status firewalld firewall-cmd --get-default-zone firewall-cmd --get-active-zones这三条命令一眼确认:服务有没有在跑,默认zone是哪个,哪些网卡绑到了什么zone。
如果firewalld本身没起来,后面一切都白搭。如果默认zone是drop,而你往public里加规则,那等于把规则写在了不同区域的房子里。
6.2 第二步:审查目标端口对应的zone规则
firewall-cmd --zone=public --list-all对照你要访问的端口,检查是否在ports列表里,是否有富规则命中,是否有service覆盖。把runtime和permanent的配置都拉出来对比,这一步能帮你排除双轨制带来的问题。
6.3 第三步:观察规则顺序和富规则之间的优先级
富规则内部有顺序。你把一条"reject all"加在"accept specific IP"前面,那后面的规则永远匹配不到。检查的时候要按添加顺序逐条阅读富规则。
6.4 第四步:检查系统层面路由和iptables的跳转
firewalld只是netfilter之上的一个管理工具。它的每条配置最终都会翻译成iptables规则。遇到"规则明明加了但还是不通"的情况,直接看iptables:
iptables -L -n -v iptables -t nat -L -n -v对照检查FORWARD链和PREROUTING链。特别是容器场景、多网卡场景,路由和转发层面的问题非常隐蔽。
6.5 第五步:检查应用自身监听地址
这是个经常被忽略的环节。你放行了8080端口,但如果应用只监听了127.0.0.1,那外部流量根本到不了应用,防火墙配置再对也没用。
ss -tlnp | grep 8080看到监听地址是127.0.0.1:8080而不是0.0.0.0:8080,说明问题在应用配置不在防火墙。把bind地址改成0.0.0.0并重启应用,问题解决。
6.6 真实排查案例
有一次,团队成员反馈"防火墙放行了3306端口,但从另一台机器连不上MySQL"。我按上面链路排查:
firewalld运行正常,默认zone是public,active-zones显示eth0绑在public。list-all里能看到3306/tcp已经放行。天然情况下不该有问题。
接着看iptables,发现MySQL的3306端口在INPUT链里没问题,但FORWARD链里出现了一条drop规则,来源方是Docker。原来MySQL跑在一个容器里,端口映射到宿主机3306。Docker的FORWARD链策略是DROP,而firewalld没有对应放行。
绕了一大圈,问题本质是Docker容器端口映射和firewalld的协作问题,回到了上一节的内容。最终方案是调整docker daemon的iptables策略,并给docker0网桥增加FORWARD规则,问题才彻底解决。
这个案例给我的体会是:排查顺序决定效率。从firewalld自身出发,逐步扩大到系统网络层,最后回到应用层,每层排查都能快速收敛问题范围,而不是一上来就抓抓包、翻日志。
7. 关于firewalld的运维习惯:我的几条私人建议
firewalld本身不复杂,复杂的场景在于它和其他网络组件协作。最后分享几条我踩过坑之后养成的运维习惯。
第一,每条规则都明确zone。不加zone的firewall-cmd默认操作的是default zone,如果你改了default-zone,命令行为会跟着变。写脚本的时候,所有命令都带--zone参数,宁可多敲几个字符,避免模糊性。
第二,把--permanent和runtime当成两个独立步骤来走,不要混在一次命令里。先runtime调试,再permanent固化,这已经是我的固定流程。调试的时候开关端口很快,固化的时候也不会误伤线上。
第三,善用日志。富规则的log前缀、firewalld自身的日志,配合journalctl,能在出问题时快速定位。Blanket放行规则在排查时只会增加噪音,尽量用精确规则替代。
第四,firewalld配置文件的版本管理。/etc/firewalld/这个目录建议纳入git等版本管理体系,每次变更都提交一次。服务器挂了重建,或者要多台机器同步规则,直接拉代码比手工敲命令可靠得多。
firewalld不是万能的,网络排查也永远不可能只靠一个工具解决所有问题。但把基础配置、zone划分、双轨制这些核心概念吃透,日常防火墙运维的大部分场景你已经能稳稳应对了。