今天早上刚到办公室,就看到群里有人在喊:“MySQL 连不上了,3306 端口不通。”我第一反应不是去看数据库,而是先问了一句:“你上个月是不是动过防火墙?”对方沉默了半分钟,回了个“好像是加过一条规则”。这种事在工程上太常见了,配置防火墙从来不是写完就完事,而是要搞清楚你写的规则到底在哪个层面生效、优先级是什么、保不保存——尤其是 CentOS 7 自带的 firewalld,它的规则体系跟老一代 iptables 的写法差异不小,很多人吃亏就吃在拿旧思路套新工具。
这篇东西我磨了很久才决定写。CentOS 7 的 firewalld 说简单也简单,无非是开放端口、限制 IP、加白名单这几件事;说复杂也复杂,因为里面有 zone、runtime/permanent、rich rule、source 和 port 这些概念绕在一起,网上零散教程不少,但能把“IP 白名单”和“端口白名单”组合起来讲透的不多。我打算把自己在服务器上实操过的完整思路、命令、踩坑记录都梳理出来,从基础概念到 3306 端口只允许内网访问这种真实场景,一次性讲明白。无论是刚入门的小白,还是被防火墙坑过几次的老手,这篇都能给你省点时间。
1. 先搞清楚 firewalld 的底细,再动手配白名单
很多人上来就敲命令,敲完发现不生效,或者不该通的反而通了,根本原因是没有理解 firewalld 的工作方式。CentOS 7 默认的防火墙已经从 iptables 换成了 firewalld,底层虽然还是 netfilter,但管理逻辑完全不同。你写的每一条规则,本质上是“动态生效”的,而且它引入了 zone(区域)的概念,这决定了你的白名单配置最终会长成什么样。
1.1 防火墙没启动时,系统到底安不安全
先说一个反直觉的事实:如果你的 firewalld 服务处于 stopped 状态,那服务器上是没有任何防火墙规则的,等于所有端口裸奔。很多人以为“我没有启动防火墙,那系统是不是默认拒绝所有连接”,这个认知在 CentOS 7 上是错的。firewalld 只是一个管理工具,它不运行,就不加载任何规则。你甚至可以直接在命令行执行systemctl status firewalld查看,如果输出是inactive (dead),那当前所有端口包括 22、3306、6379,都是对外暴露的。
所以验防火墙问题的第一步永远不是改规则,而是先确认服务状态:
systemctl status firewalld systemctl is-enabled firewalld第一条看运行状态,第二条看开机自启状态。我在实际排障中发现,很多“端口不通”的问题根本不是白名单没配,而是 firewalld 压根没启动,或者启动了但规则临时清空了。这里补一句:firewalld 正常启动后默认的 public zone 会放行 SSH(22 端口)以及 DHCP、ICMP 这些基础协议,其他端口默认拒绝。你要是上手就把 public zone 改成 drop,那 SSH 也可能断,这是新手最容易犯的错。
1.2 区域(zone)概念是白名单配置的地基
firewalld 里的 zone 相当于一组策略的集合,每个 zone 有自己的默认行为:允许什么、拒绝什么、转发什么。系统默认有九个 zone,最常用的是这三个:
| Zone名称 | 默认行为 | 适用场景 |
|---|---|---|
| public | 只放行指定服务和端口,其余拒绝 | 外网服务器默认区域 |
| trusted | 所有连接都接受 | 内网、可信网段 |
| drop | 直接丢弃所有入站连接 | 极端安全场景 |
配置白名单这件事,本质上就是“把一个来源 IP 网段归属到某个 zone”,或者“在某个 zone 里对特定来源 IP 放行特定端口”。很多人不理解 source 的优先级,这里我强调一个关键点:当一个连接同时命中了网卡绑定的 zone 和来源 IP 绑定的 zone 时,来源 IP 绑定的 zone 优先级更高。比如网卡 eth0 默认在 public zone,你额外把 192.168.1.0/24 加到了 trusted zone,那么来自这个网段的流量会走 trusted 的规则,而不是 public 的规则。这个特性特别适合白名单场景,后面实战部分我再细说。
2. 开放端口的基础操作,别一上来就问白名单
我遇到过不少人,需求一开口就是“我要配白名单”,但一问具体要放行什么服务,自己都说不清楚。配白名单之前,你至少得先知道端口怎么放行,因为很多场景下“仅白名单可访问”就是在“开放端口”的基础上叠加来源限制。先把手动开放端口的操作垫扎实,后面才能少踩坑。
2.1 一条命令开放 TCP 端口,但要注意 permanent
开放单个端口的标准命令是:
firewall-cmd --zone=public --add-port=8080/tcp这条命令会立刻生效,但只在当前运行时有效,服务器重启或者 firewalld reload 之后就没了。想永久生效,必须加上--permanent参数,然后执行 reload:
firewall-cmd --zone=public --add-port=8080/tcp --permanent firewall-cmd --reload顺序上有个小细节:如果是先执行了不带--permanent的命令,再执行带--permanent的命令,两个都会存在,一个是 runtime 状态,一个是 permanent 配置。你不 reload 还好,一 reload,runtime 状态的规则就全丢了,只有 permanent 的还在。所以我的习惯是:要么全程只操作 runtime,测试没问题后再全部加 permanent;要么从一开始就统一带--permanent。最怕的是两条命令混着敲,自己都分不清哪条是临时哪条是永久。
2.2 端口范围、UDP 端口以及批量添加的讲究
有时候你需要放行的不止一个端口,比如 Web 服务的 8000 到 9000,或者游戏服务器的 UDP 范围。firewalld 支持端口段写法:
firewall-cmd --zone=public --add-port=8000-9000/tcp --permanent firewall-cmd --zone=public --add-port=1000-2000/udp --permanent端口段和单个端口写法几乎一样,只需要把端口改成起始-结束的格式。但需要注意:TCP 和 UDP 是两条独立的规则,你不能写--add-port=53/tcp+udp,必须分别添加。批量添加多个不连续端口时,firewalld 没有类似--add-ports的复数参数,我是用 shell 循环处理的:
for port in 8080 8443 9090; do firewall-cmd --zone=public --add-port=${port}/tcp --permanent done firewall-cmd --reload这里有个经验之谈:批量添加后列出所有规则时,你会看到每个端口单独一条记录,规则数量多了以后不太好维护。所以端口规划很重要,能合并成端口段的尽量合并,否则后面查问题的时候,firewall-cmd --list-all输出的规则列表能刷你两屏。
2.3 验证端口放行是否生效,别靠“感觉”
命令敲完,规则也加了,但端口到底通不通,不能靠猜。我一般分三步验证:
第一步,看端口是否在监听,确认是服务本身在监听,而不是防火墙的问题:
ss -lntp | grep 8080第二步,本机验证防火墙规则是否已加载:
firewall-cmd --zone=public --list-ports第三步,从外部机器测试端口连通性:
telnet 192.168.1.10 8080 # 或者 nc -vz 192.168.1.10 8080这一步很多人容易忽略,总在服务器上自己测自己,其实测的是服务监听,不是防火墙。正确做法是找一台别的机器去连,或者用你本地的电脑执行 telnet。如果你服务器上没装 telnet,也可以用curl -v telnet://192.168.1.10:8080测,但最简单粗暴的还是 nc。验证这一步做好,后面配白名单的时候才能确定问题到底出在哪一环。
3. IP 白名单:从单个地址到整段内网
端口放行是“打开门”,IP 白名单是“只让特定的人进门”。在 firewalld 里实现 IP 白名单有几种思路,每种思路的适用场景不太一样,配置方式也不同。我建议你先想清楚自己的需求是“某个来源 IP 可以访问服务器的所有服务”还是“某个来源 IP 只能访问某一个端口”,这两种场景对应的命令差别很大。
3.1 把单个来源 IP 加入白名单
如果你只是想让某个固定的公网 IP 能访问服务器,最简单的做法是把该 IP 加入白名单区域:
firewall-cmd --permanent --zone=trusted --add-source=203.0.113.5 firewall-cmd --reload这样 203.0.113.5 这个 IP 访问服务器时,流量会命中 trusted zone 的规则,trusted 默认信任所有连接。这个命令看起来很省事,但你得清楚一个副作用:这个 IP 访问服务器的所有端口都会被放行,不仅仅是 22 或 80。如果你只想让这个 IP 访问特定的服务,千万别直接把 IP 扔进 trusted,否则安全边界就没了。我见过有人为了省事,把合作方的 IP 全加进 trusted,结果对方能直接访问 Redis 端口,还好 Redis 配置了 requirepass,不然后果不堪设想。
如果不想用 trusted 这种全放行的方式,可以留在 public zone,单独添加来源地址,效果是“这个来源的流量按 public zone 的规则处理”:
firewall-cmd --permanent --zone=public --add-source=203.0.113.5 firewall-cmd --reload这样设置之后,public zone 里放行的端口(比如 80、443)对这个 IP 有效,其他未放行端口依然拒绝。这两种方式用哪个,取决于你对“白名单”的定义是“全部放行”还是“按规则放行”。
3.2 配置整段 IP 网段白名单
很多时候你面对的不止一个 IP,而是一个网段,比如公司办公网可能是一个 192.168.1.0/24 的地址池。配置整段网段白名单和单个 IP 的语法几乎一样,只需要把 IP 地址改成 CIDR 格式:
firewall-cmd --permanent --zone=trusted --add-source=192.168.1.0/24 firewall-cmd --reload这里我特别提醒一个点:192.168.1.0/24和192.168.1.1/24写成哪种都能命中整个网段,因为 CIDR 的匹配只看网络位。但养成用.0作为网段地址的习惯更规范,也方便别人看懂你的规则。如果你的内网分多个网段,比如办公网 192.168.1.0/24、测试网 192.168.2.0/24、运维网 192.168.3.0/24,你可以一次性加多个 source:
firewall-cmd --permanent --zone=trusted --add-source=192.168.1.0/24 firewall-cmd --permanent --zone=trusted --add-source=192.168.2.0/24 firewall-cmd --permanent --zone=trusted --add-source=192.168.3.0/24 firewall-cmd --reload这么配的优点是规则简单清晰,运维同事一看就懂,排查问题不用猜。缺点是 trusted zone 对这些网段全放行,内部安全靠内网自己的安全措施去兜底。
3.3 用网卡绑定 zone 来区分内外网
还有一类场景比较特殊:服务器有两张网卡,一张接内网,一张接外网。这种情况下,最好的方案不是用 source 白名单,而是让不同网卡归属不同 zone。内网网卡绑 trusted,外网网卡绑 public,这样所有从内网卡进来的流量天然可信,外网卡继续保持严格过滤。
查看当前网卡和 zone 的绑定关系:
firewall-cmd --get-active-zones把指定网卡绑定到指定 zone:
firewall-cmd --permanent --zone=trusted --change-interface=eth1 firewall-cmd --permanent --zone=public --change-interface=eth0 firewall-cmd --reload这种方式比单纯加 source 更细粒度,而且在有多网卡的物理服务器或带多网口的云主机上非常实用。不过有个前提条件:你要确认服务器到内网其他机器的流量确实走的是 eth1,如果路由写得不清楚,流量从 eth0 出去了,那绑定 eth1 到 trusted 就白搭了。配置完可以用ip route看默认路由走哪张网卡,再配合 tcpdump 抓包确认。
4. 端口级白名单:指定端口只允许指定 IP 访问
前面几步讲的都是“放开某个 IP 或网段”,接下来这个才是真正的重头戏:某端口只允许指定的 IP 访问,其他来源一律拒绝。比如 MySQL 3306 端口只允许应用服务器 IP 访问,或者管理面板 9090 端口只允许公司办公网访问。这类需求靠普通的--add-port和--add-source配不出来,需要用 firewalld 的富规则,也就是 rich rule。
4.1 rich rule 语法与常用写法
rich rule 是 firewalld 提供的高级规则语法,支持按来源地址、目的端口、协议、动作(accept/reject/drop)组合出一个完整的访问控制策略。最简单的写法如下:
firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.100" port protocol="tcp" port="3306" accept' firewall-cmd --reload这条规则表达的意思是:来源 IP 为 192.168.1.100 的机器访问本机 TCP 3306 端口时,允许通过。其他来源访问 3306 端口时,如果 public zone 没有设置放行 3306,会被默认拒绝。
rich rule 的语法有几个细节容易踩坑。第一,family="ipv4"和source address必须搭配,如果你不写family,规则默认只对 IPv4 生效,这倒没问题,但如果你写family="ipv6"又配上 IPv4 地址,规则直接不生效。第二,port和protocol必须同时出现,protocol="tcp"和port="3306"两个参数缺一不可。第三,rich rule 里的accept是优先级最高的动作,一旦来源 IP 命中这个规则,直接放行,不会再看后面的默认策略。
4.2 一个端口、多个白名单 IP 段怎么配
端口级白名单遇到多个 IP 或网段时,可以写多条 rich rule,一条对应一个来源。比如数据库 3306 端口需要同时允许 192.168.1.0/24 和 10.0.0.5 访问:
firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port protocol="tcp" port="3306" accept' firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="10.0.0.5" port protocol="tcp" port="3306" accept' firewall-cmd --reload这种方式直白,每条规则独立,后续要移除某个 IP 段也很方便:
firewall-cmd --permanent --zone=public --remove-rich-rule='rule family="ipv4" source address="10.0.0.5" port protocol="tcp" port="3306" accept' firewall-cmd --reload不过,当白名单 IP 特别多的时候,比如上百个,逐条写 rich rule 就太啰嗦了。这种情况我建议用 ipset 来管理,先在系统层面创建一个 ipset 集合,然后把所有白名单 IP 都加进去,最后在 rich rule 里直接引用这个集合。具体做法是:
ipset create allow_mysql_ips hash:ip ipset add allow_mysql_ips 192.168.1.100 ipset add allow_mysql_ips 10.0.0.5然后在 firewalld 里引用 ipset:
firewall-cmd --permanent --zone=public --add-rich-rule='rule source ipset="allow_mysql_ips" port protocol="tcp" port="3306" accept' firewall-cmd --reloadipset 的优势是规则只写一条,后续增删 IP 只需要操作 ipset,不用频繁 reload 防火墙,对生产环境很友好。
4.3 先拒绝所有来源,再放行白名单
还有一类需求更严格:某端口不允许任何来源访问,除非来源 IP 在白名单里。这里的一个坑在于,如果你之前用--add-port已经把该端口放开了,那么 rich rule 里的reject可能并不会像你想的那样“覆盖”之前的放行规则。
我实际操作中最稳妥的方式是两步走。第一步,移除 public zone 里对该端口的通用放行规则:
firewall-cmd --permanent --zone=public --remove-port=3306/tcp第二步,用 rich rule 放行白名单 IP:
firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port protocol="tcp" port="3306" accept'这样处理之后,public zone 里没有针对 3306 的通用放行规则,默认拒绝所有来源;而 rich rule 会让 192.168.1.0/24 网段的流量直接命中 accept。很多教程只讲第二步,不讲第一步,导致有人配完白名单之后发现其他 IP 还能访问端口,其实就是因为端口本身已经被--add-port放行了。如果你希望操作更透明、可追溯,我建议在配置端口级白名单前,先用firewall-cmd --list-all看看当前 zone 里已有的规则,确认端口有没有被通用放行。这个习惯能帮你省掉很多查问题的时间。
5. 实战:MySQL 3306 端口只允许内网网段访问
很多后台系统的架构里,数据库服务器和应用服务器是分离的,应用服务器需要远程连接数据库,但数据库绝对不能对公网开放。这种场景下,你需要配置的就是“3306 端口只允许应用服务器所在网段的 IP 访问”,这也是我运维生涯里遇到最多的白名单需求之一。
5.1 需求拆解与方案设计
假设当前环境如下:数据库服务器 IP 为 172.16.0.10,应用服务器网段为 172.16.1.0/24,运维人员办公网段为 10.10.10.0/24,root 用户通过堡垒机登录数据库服务器做日常运维。需求是 3306 端口只能被 172.16.1.0/24 访问,运维人员如果需要直连数据库,则通过堡垒机再跳转,所以 10.10.10.0/24 不需要直接放行 3306。
这种场景如果我直接给 172.16.1.0/24 配一个 trusted zone,那等于这个网段的机器能访问数据库服务器上所有端口,安全边界太粗。正确的设计是:数据库服务器保持在 public zone,通过 rich rule 只放行 172.16.1.0/24 访问 3306 端口,其余流量继续走 public zone 的默认策略。这样的话,即使应用服务器网段里有一台机器被入侵了,它也只能访问 3306 端口,无法横向触碰服务器上的其他服务。
5.2 完整配置步骤
执行前先确认当前防火墙状态和已有规则,避免旧规则干扰:
systemctl status firewalld firewall-cmd --list-all如果当前已有对 3306 端口的通用放行规则,先移除:
firewall-cmd --permanent --zone=public --remove-port=3306/tcp然后添加 rich rule,放行应用服务器网段:
firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="172.16.1.0/24" port protocol="tcp" port="3306" accept'重载防火墙,并确认规则已生效:
firewall-cmd --reload firewall-cmd --list-all firewall-cmd --zone=public --list-rich-rules这一步我通常会多看一眼--list-rich-rules的输出,确认规则没有被写错,尤其是引号、空格这样的细节。rich rule 是整体作为一个参数传给 firewall-cmd 的,一旦引号没配对,命令要么报错,要么写出一条和你预期完全不同的规则。还要注意,如果服务器上装了 MySQL,你确认端口在监听:
ss -lntp | grep 3306如果 MySQL 没启动或者监听在 127.0.0.1 而不是 0.0.0.0,那防火墙配得再对,应用服务器也连不上。
5.3 验证与回滚
配置完成后,我从三台不同位置的机器分别测试连通性:一台在 172.16.1.0/24 网段内、一台在 172.16.2.0/24 网段外、一台在数据库服务器本机。如果是 172.16.1.0/24 内的机器,执行:
mysql -h172.16.0.10 -P3306 -uroot -p预期结果是能正常连接。如果是 172.16.2.0/24 的机器,执行同样的命令,预期结果是连接超时或直接被拒绝。这里注意,如果连接超时的等待时间很长,多半是流量被 drop 了,如果立刻提示拒绝,则是被 reject。firewalld 的默认动作里,public zone 对未放行端口的处理是 reject,会立刻返回 connection refused,所以在外部测试时看到这个提示,反倒说明防火墙规则是生效的。
如果验证过程中发现规则有问题,需要回滚,操作也很直接,把刚才添加的 rich rule 移除即可:
firewall-cmd --permanent --zone=public --remove-rich-rule='rule family="ipv4" source address="172.16.1.0/24" port protocol="tcp" port="3306" accept' firewall-cmd --reload我建议你在生产环境做类似操作时,先把新规则加上并确认没问题后,再用--reload切换,这样如果规则写错了,旧的 runtime 规则还在,还能快速恢复。复杂防火墙变更前,最好先把/etc/firewalld/zones/目录备份一下,这条路径存放了所有自定义 zone 配置,备份成本很低,但出问题时能救命。
6. 配置白名单时踩过的坑和排查思路
firewalld 配置本身不难,难的是出了问题怎么查、怎么恢复。我把自己这几年实际踩过的坑整理了一份排查清单,按问题出现的频率排序,你在生产环境操作时可以直接对照参考。
6.1 配置了白名单但外网还是能访问,先查这两处
遇到这种问题,我从来不去纠结防火墙命令有没有写对,而是先执行:
firewall-cmd --list-all --zone=public重点看两处:一是该端口是不是已经通过--add-port放行了,二是 rich rule 是不是真的在列表里。如果端口本身被--add-port放行,rich rule 里的reject不会覆盖它,流量依然能通过。如果 rich rule 在列表里,那就要检查是不是配置到了错误的 zone,比如你明明想限制 public zone,但实际规则加到了 trusted zone。可以用下面命令查所有 zone 的规则:
firewall-cmd --list-all-zones还有一个容易忽略的点:如果服务器上同时运行着 Docker,Docker 的 iptables 规则可能会绕过 firewalld,直接操作 NETWORK 层的规则。这种场景下,即使你在 firewalld 里配置了拒绝规则,Docker 容器发布到宿主机的端口可能依然能从外部访问。这个问题的根治方法比较麻烦,涉及 Docker 的 iptables 管理方式,但至少排查思路要清楚:先看iptables -L -n有没有 Docker 插入的规则,再判断是不是 firewalld 被架空了。
6.2 permanent 和 runtime 规则不一致,导致 reload 后规则丢失
firewalld 启动时加载 permanent 配置,运行过程中可以临时添加 runtime 规则。如果你忘记加--permanent,直接执行了--reload,运行时规则全部丢失。这个坑在操作频繁的服务器上特别常见,尤其是先测试后落地的工作流里。
我的建议很简单,调整防火墙规则时,要么全部带--permanent,要么明确区分临时和永久。如果只是想临时放行某个端口测试五分钟,那就不要带--permanent,测完直接移除;如果是正式配置,就一步到位加--permanent,然后 reload。最忌讳的是混着用还不做记录,等出了事故,你根本分不清当前规则哪些是永久的、哪些是临时的。
6.3 远程操作防火墙导致 SSH 断连,如何恢复
这是防火墙配置里最经典的事故之一。你在远程服务器上执行了一条防火墙规则,把 SSH 端口关了或者把当前 IP 拒绝了,结果连接瞬间断开,人直接进不去了。这种情况如果云厂商控制台有 VNC 或网页终端,可以用那个登录;如果没有,那就只能重启服务器,让防火墙重新加载 permanent 配置。
不过更靠谱的是预防手段。第一,确保 SSH 端口始终在 public zone 的 services 列表里:
firewall-cmd --permanent --zone=public --add-service=ssh第二,如果你要限制 SSH 只允许特定 IP 访问,建议先加白名单规则再加拒绝规则,而且两步之间不要立即 reload,确认没问题再 reload。我的习惯是把ssh这个 service 保留在 public zone 里,用 source 白名单限制来源,而不是直接把ssh从 service 列表里删掉,这样即使白名单配置出错,至少还有sshservice 在兜底。
6.4 用 ipset 提升大量 IP 的管理效率
前面提过 ipset,这里再展开一点。当白名单 IP 数量上了几十个以后,逐条写 rich rule 会变得很痛苦,规则的显示输出也很长。ipset 的方案更优雅:
yum install -y ipset ipset create allow_ssh_ips hash:ip ipset add allow_ssh_ips 203.0.113.1 ipset add allow_ssh_ips 203.0.113.2然后在 rich rule 里引用 ipset:
firewall-cmd --permanent --zone=public --add-rich-rule='rule source ipset="allow_ssh_ips" service name="ssh" accept'这样后续增加白名单 IP,只需要执行ipset add,不用动防火墙配置。而且 ipset 支持 hash:net 类型,可以存网段:
ipset create allow_networks hash:net ipset add allow_networks 192.168.1.0/24对于经常变动的白名单列表,ipset 的维护成本比 rich rule 低得多。不过需要提醒的是,ipset 集合本身不持久化,服务器重启后 ipset 列表会清空,所以在生产环境中最好把 ipset 的创建和添加命令写进启动脚本,或者用 systemd 服务来管理,否则重启后白名单就失效了。
6.5 清理规则的正确姿势:先查后删
最后再分享一个日常维护建议。防火墙规则是会累积的,时间一长,你自己都可能忘了当初为什么加某条规则。所以我的习惯是每隔一段时间就执行一次firewall-cmd --list-all-zones,把所有规则过一遍,看到已经不用的端口白名单、已经下线的网段,就顺手清理掉。删除规则的命令和添加规则基本对称:
firewall-cmd --permanent --zone=public --remove-rich-rule='rule family="ipv4" source address="192.168.1.100" port protocol="tcp" port="3306" accept' firewall-cmd --permanent --zone=public --remove-source=192.168.1.0/24 firewall-cmd --reload在执行删除之前,我建议先看一眼规则内容,用firewall-cmd --zone=public --list-rich-rules查全部富规则,确认要删的是哪一条,避免手滑把别的白名单误删了。清理完再列一次规则,确保结果符合预期。这个习惯看着简单,但真能在关键时刻避免事故——我就见过有人清理规则时,把整个 zone 的规则清空了,导致线上服务端口全开、裸奔了一晚上。