iptables规则保存与持久化:重启不丢的完整指南

发布时间:2026/9/13 1:23:58
iptables规则保存与持久化:重启不丢的完整指南 1. 重启后规则全丢iptables的内存型机制到底是怎么一回事我最早用 iptables 配规则的时候干过一件特别蠢的事花了一下午写了一套非常满意的规则集当时测试全通过顺手就下班了。第二天早上到工位远程 SSH 一登心里还美滋滋的结果 iptables -L -n -v 一看表里干干净净一条不剩。那一瞬间是真的后背发凉——因为我在那台服务器上跑了线上业务脑子里全是昨晚是不是被人入侵清掉了规则的念头。后查了系统日志才弄明白压根没人黑我就是规则没保存。这个问题的根源得从 iptables 的架构说起。iptables 本身是一个用户态的命令行工具它真正干的事是跟内核里的 netfilter 框架打交道。netfilter 是 Linux 内核网络协议栈里面的一组钩子而 iptables 的规则其实就是挂在这组钩子上面的处理条目。你执行一条 iptables 命令本质上是在内存里修改这套处理逻辑。换句话说规则活在内存里活在运行中的内核里它不在磁盘上也没有任何自动落盘的机制。这就是为什么很多人第一次上手 iptables 时会懵——你明明已经写好规则了防火墙也确实按规则在工作可一旦 reboot或者 systemctl restart iptables一切归零。它不像 firewalld 那样改一下就同步配置文件。iptables 更接近原生命令它只管当前会话内有效不替你操心持久化。这个内存型设计其实有它的历史原因。当年 netfilter 框架诞生的时候主要目标是给内核提供灵活高效的报文处理能力而不是做成一个带持久化配置的防火墙产品。持久化这件事本来是交给上层发行版来做。不同发行版做了不同方案这也是后文要讲的重点。但有一点是共通的只要你想把规则留住就必须自己动手做保存这一步。从运维角度来理解这件事可以这样类比iptables 规则就像你在终端里 export 的临时环境变量当前 shell 里有效shell 一关就没了。想要永久生效你得把它写进 .bashrc 或者其他 profile 文件里。iptables 的情况完全一样只不过它要写的不是 .bashrc而是各发行版约定的规则文件。理解了这一层也就理解了为什么教程里总强调测试完立刻保存。因为从规则落地到真正生效之间隔着一个重启的距离这个距离一旦碰上断电、内核升级、误操作重启就是事故现场。后面我会分别讲 CentOS/RHEL 系、Debian/Ubuntu 系的保存方案以及一种跟发行版无关的通用做法再把规则文件的内容拆开讲一讲。最后是我这些年踩过的坑每条都配了具体的排查思路希望能让你少走弯路。2. 主流发行版的三类保存方案service版、persistent版、通用脚本iptables 规则保存这件事不同发行版给出了完全不同的答案。我见过不少人在 CentOS 上学了一套方法换到 Ubuntu 上还想用同样的命令结果发现压根没这个命令或者服务名都不一样一头雾水。所以这一节把三类主流方案拆开讲清楚。2.1 RHEL/CentOS 系iptables-services 与 /etc/sysconfig/iptablesRHEL/CentOS 6 时代以及 7 的初始阶段大家最常用的保存方式是 iptables 自带的命令service iptables save或者iptables-save /etc/sysconfig/iptablesCentOS 7 默认发行版里iptables 命令本身是存在的但默认防火墙换成了 firewalld。如果要用传统 iptables必须装一个叫 iptables-services 的包yum install -y iptables-services systemctl stop firewalld systemctl disable firewalld systemctl enable iptables systemctl start iptables装上之后service iptables save 这条命令才真正可用。它内部做的事就是调用 iptables-save把当前内存里的规则导出到 /etc/sysconfig/iptables 这个文件。等你下次开机systemd 会启动 iptables.service它读取 /etc/sysconfig/iptables 文件把里面的规则逐条 load 回内核。这里有个细节值得说一句iptables-services 这个包提供的 iptables.service 在启动时加载规则文件在停止时会把当前规则再导出一遍到 /etc/sysconfig/iptables。所以你在 CentOS 7 上如果执行 systemctl stop iptables它不光停服务还会把规则文件覆盖成当前内存状态。这在某些场景下会让人困惑比如你想先停掉防火墙看看效果结果规则文件被自动固化成了当前的规则后面再启动时加载的是这份新文件——不是你以为的旧的完整规则。这点后面排错章节会再次提到。2.2 Debian/Ubuntu 系iptables-persistent 与 /etc/iptables/rules.v4Ubuntu 上最主流的方式是装一个叫 iptables-persistent 的包apt update apt install -y iptables-persistent安装过程中它会问你是否保存当前规则选是就会帮你生成 /etc/iptables/rules.v4 和 /etc/iptables/rules.v6IPv6 规则文件。之后你再修改规则只需要执行netfilter-persistent save或者更老一点的写法iptables-save /etc/iptables/rules.v4 ip6tables-save /etc/iptables/rules.v6netfilter-persistent 这个命令实际干的事就是调用 iptables-save 和 ip6tables-save把内存规则分别写到 rules.v4 和 rules.v6 里。系统启动时netfilter-persistent.service 会根据这两个文件恢复规则。这里插一句在 Ubuntu 18.04 及之后的系统上netfilter-persistent.service 默认是 enabled 的但你如果先装了系统后跑业务再回来装 iptables-persistent它默认只保存安装那一刻的规则。如果安装前你已经配了一堆规则安装时又选了否那安装完还得手动 save 一次不然重启照样丢。2.3 通用兜底方案iptables-save/iptables-restore 手动操作如果你用的发行版比较小众或者你压根不想依赖发行版的服务管理方式那就直接用这对组合# 保存把当前规则导出到指定文件 iptables-save /root/iptables.rules # 恢复从文件导入规则 iptables-restore /root/iptables.rules这一招的好处是跟发行版无关只要机器上有 iptables 命令就能干。至于开机自动恢复最简单的方式是写一个 systemd service 单元或者把 iptables-restore 写进 rc.local。我实际更常用的是后一种组合原因有两个一是生产环境里我喜欢把规则文件放在自己约定的路径下不想被发行版的文件路径绑死二是用脚本管理规则集时iptables-save/restore 的文本格式更方便做版本管理——直接把 .rules 文件丢进 Git改没改动一目了然回滚也方便。不过要注意iptables-restore 是全量覆盖不是增量追加。执行 restore 时它会先清空当前规则再导入文件里的规则。这不是什么 bug而是设计的预期行为。反过来想这其实方便了我们——保存和恢复的过程是严格对称的不会出现旧规则没清掉、新规则又叠加的混乱局面。3. 规则文件内容拆解每一行到底在表达什么3.1 三种表、五条链规则文件的整体结构用 iptables-save 导出的文件格式非常规整乍一看像某种配置文件其实它就是对内存规则表的逐行序列化。一个典型的 /etc/sysconfig/iptables 文件长这样# Generated by iptables-save v1.8.4 on Wed Dec 11 10:23:45 2024 *filter :INPUT DROP [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] -A INPUT -i lo -j ACCEPT -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT -A INPUT -p tcp --dport 22 -j ACCEPT -A INPUT -p tcp --dport 443 -j ACCEPT COMMIT # Completed on Wed Dec 11 10:23:45 2024逐行拆开看就非常清晰了*filter表示接下来是 filter 表的规则。iptables 的规则是分表的最常见的是 filter过滤、nat地址转换、mangle报文修改。一次 iptables-save 导出会把所有表都列出来表之间用*表名开头用COMMIT结束。:INPUT DROP [0:0]是链的声明。冒号后面是链名再往后是这个链的默认策略policy方括号里的是该链当前被匹配到的包计数和字节数packets/bytes。注意[0:0]只是计数不是你设的值是你当前内核里累计的统计数值导出时一并带上了。-A INPUT ...就是实际的规则条目跟命令行里写的参数完全一致。COMMIT表示这一张表的规则提交生效是表内容的结束标志。如果有 nat 表文件里还会出现*nat段里面是:PREROUTING、:POSTROUTING、:OUTPUT等链以及 MASQUERADE、DNAT、SNAT 这些动作。3.2 恢复时的加载顺序为什么 COMMIT 必须放在最后iptables-restore 在解析这个文件的时候是逐行处理的先读表声明再读链声明再逐条读规则碰到 COMMIT 才真正把前面累积的规则一次性提交到内核。这个设计细想一下是很有讲究的——它让批量导入变成事务性操作要么全部成功要么全部失败不会出现导入到一半系统状态变得不三不四的情况。但这也意味着如果规则文件本身有语法错误iptables-restore 会在那个位置报错并且整份文件都不会生效。不会出现前半部分导入了、后半部分失败的中间态这对运维来说是件好事至少状态可控。我自己在手动编辑规则文件时养成了一个习惯写完先用 iptables-restore --test 做一次语法检查确认没有报错再真正加载。这个 --test 参数会解析文件内容但并不真正写入内核非常适合写完规则后做自检。3.3 自定义链的坑如果你的规则用了 -N如果你在命令行里创建了自定义链比如iptables -N MYCHAIN iptables -A MYCHAIN -p tcp --dport 8080 -j ACCEPT iptables -A INPUT -j MYCHAIN那规则文件里会出现类似于:MYCHAIN - [0:0]的链声明。这种情况下链声明的顺序是有讲究的自定义链必须先声明后面才能引用。iptables-save 导出时顺序是自动排好的但如果你手动编辑文件并调整了行顺序就可能出现引用了一个还没定义的链的错误。iptables-restore 对这种情况的报错信息算是比较直观的会提示 unknown chain。不过等到出错再回头找原因就不如从一开始就把链声明放前面来得省心。4. 保存操作全流程从清空旧规则到验证加载这一节按一套完整的操作流程走一遍从零开始到规则固化到开机自启每一步都说明为什么这么做。如果你已经有一台配好的服务器可以直接对照操作。4.1 先想清楚清空规则时别忘了检查链的默认策略清空规则几乎是每次重写规则集的必经步骤。常见的做法是iptables -F iptables -X iptables -Z-F 是清空链里的规则-X 是删除自定义链-Z 是把计数器清零。但这里有一个特别容易被忽略的点-F 不清链的默认策略policy。如果你的 INPUT 链默认策略是 DROP那你清空规则的那一刻所有入站连接就会立即被断掉——包括你的 SSH 会话。我之前有一次在远程服务器上就吃过这个亏想清掉旧规则重新写执行完 iptables -F 之后SSH 立刻断开怎么重连都连不上后面只能让人帮忙重启机器。原因就是 INPUT 链的 policy 是 DROP规则一清连它回你 SYN 包都不发了。所以在执行清空操作之前先看一眼当前链默认策略iptables -L INPUT -n --line-numbers如果显示policy DROP你要么先临时把策略改成 ACCEPTiptables -P INPUT ACCEPT要么确认自己有带外访问手段比如机房 IPMI、云平台 VNC。否则别轻易在远程状态下清空规则。正确的重置姿势应该是# 先放开默认策略再清规则最后清计数器 iptables -P INPUT ACCEPT iptables -P FORWARD ACCEPT iptables -P OUTPUT ACCEPT iptables -F iptables -X iptables -Z4.2 在线测试新规则给自己留一条后路配置新规则的时候我习惯把风险操作降到最低方法是分步骤测试先加一条接受所有已建立连接的规则确保现有 SSH 会话不受影响。再加允许 SSH 端口22的新连接规则。验证 SSH 端口放行没问题再加其他业务规则。全部规则加完后开一个新终端窗口试连确认没问题再执行保存。这个顺序不是随便定的。第一条规则保证了你当前会话的连续性第二条保证了你能重新连进来有了这两条保底后面怎么折腾都不至于把自己锁在外面。测试时还有一个常用技巧把规则写到一个文件里然后用 iptables-restore 来加载而不是一条一条手动敲。这样出了问题可以用之前导出的备份文件快速恢复现场。# 把当前规则备份下来作为回滚点 iptables-save /root/iptables.backup.$(date %F_%H%M%S) # 加载新的规则文件 iptables-restore /root/iptables-new.rules如果连不上直接iptables-restore /root/iptables.backup.20241211_153000恢复到之前的可用状态。这个先备份再改的思路在任何配置变更场景下都适用尤其适合线上服务器。4.3 保存并验证重启之后规则确实在规则测完没问题接下来就是保存。以 CentOS 系为例service iptables save或者更直接一点iptables-save /etc/sysconfig/iptables保存完成后一定要做两项验证缺一不可。第一项检查文件内容非空、非乱码。这一步看起来多余但文件路径写错、权限不对导致保存失败的情况我遇见过不止一次。确认一下文件大小不是 0再 head 看一下开头几行是否正常ls -lh /etc/sysconfig/iptables head -n 20 /etc/sysconfig/iptables第二项模拟重启后的情况确认规则能恢复。方法是先把当前所有规则清掉然后手动执行 iptables-restore 加载文件iptables -F iptables -X iptables-restore /etc/sysconfig/iptables如果加载后 iptables -L -n -v 显示的内容跟预期一致说明文件本身没问题开机自启服务也只是做了同样的事而已。这一步验证完你就可以放心重启服务器了。Ubuntu/Debian 系对应的验证方式是netfilter-persistent save cat /etc/iptables/rules.v4 netfilter-persistent flush # 清空当前规则 netfilter-persistent start # 从文件加载规则这里的 flush 参数会清空所有规则start 会重新加载 rules.v4 和 rules.v6正好用于验证。5. 这些年我踩过的坑规则保存成功了重启后还是不生效5.1 firewalld 和 iptables 抢同一个文件CentOS 7 之后最常见的翻车现场是你明明装了 iptables-services也 service iptables save 了但重启之后规则还是丢了。查了半天最后发现是 firewalld 还开着或者 iptables.service 压根没 enable。你可能会说iptables 命令能用规则也正常怎么服务没启动呢这里有个认知陷阱CentOS 7 里执行 iptables 命令跟启动 iptables.service 是两回事。iptables 命令行工具是内核 netfilter 的客户端它跟 systemd 服务没有任何绑定关系。即使 iptables.service 处于 disabled 状态你一样可以手动执行 iptables 命令加规则规则也会立刻生效——但重启后不会恢复因为这个服务没被设置为开机启动。排查方法很简单systemctl status iptables systemctl is-enabled iptables如果是 disabled把它设成 enabled。同时确认 firewalld 是停止且禁用的systemctl disable firewalld systemctl stop firewalldfirewalld 和 iptables-services 不能同时启用因为两者都会往内核 netfilter 挂规则而且 firewalld 服务启动时也会清空/加载自己的规则集产生冲突时行为很难预测我在实验环境里见过两边规则互相覆盖的情况线上环境最好别冒这个险。5.2 网卡名变化导致规则失效这一条是真正让我长记性的。一台机器从旧平台迁移到新平台或者是 CentOS 6 升到 7网卡名从 eth0 变成了 ens33规则文件里还写着-A INPUT -i eth0 ...一加载匹配不上任何流量跟没写一样。这事的本质是iptables 规则里引用网卡名时是动态解析的。如果规则加载时名为 eth0 的接口不存在这条规则虽然不会被报错但也永远不会匹配到任何包。重启后网络接口的命名因内核参数或 udev 规则变化而改变所有绑定旧网卡名的规则就全成了摆设。排查方向遇到规则看着在但流量行为不对的情况第一条命令就是ip link show看看当前实际存在的网卡接口名再对比规则文件里 -i、-o 参数后面的接口名是否一致。更深一层的坑是如果你的规则文件里既写 eth0 又写 eth1但系统上只有 eth0那么 eth1 相关的规则无效而 eth0 的规则正常。这种部分规则失效比全失效更容易让人误判因为 iptables -L 看起来还是有规则在那里的。5.3 重启后规则加载顺序错误有些人的规则文件里既有放行规则又有拒绝规则写的时候顺序是乱的。iptables 的规则匹配是顺序执行的——一旦某条规则匹配后执行了某个动作ACCEPT/DROP后面的规则就不会再被检查了。举例来说-A INPUT -p tcp --dport 3306 -j DROP -A INPUT -p tcp --dport 3306 -s 192.168.1.0/24 -j ACCEPT这两条规则的意图可能是默认拒绝外部访问 3306但允许内网段访问。但按这个顺序一旦前一条 DROP 先匹配了后面的 ACCEPT 永远不会执行——即使源地址是 192.168.1.0/24 也一样会被丢掉。规则确实是保存了保存得还很完整但逻辑上就是错的。所以保存之前一定要按顺序即逻辑的标准审查一遍规则文件。我常用的审查方式很简单把 iptables-save 导出的文件按链分开看逐条读出来。规则不多的时候肉眼排序就够规则多了就会体会到把规则写成脚本模板的好处这个后面讲。5.4 iptables-restore 与 ip6tables-restore 的遗漏这个坑在双栈服务器上比较隐蔽。Debian/Ubuntu 系的 iptables-persistent 会同时保存 v4 和 v6 规则但如果你是自己写 systemd 单元调用 iptables-restore很容易只写了 IPv4 的规则IPv6 链路完全裸奔。我之前有一台服务器v4 规则的 INPUT 链策略是 DROPv6 的 INPUT 链策略是默认 ACCEPT等于 ssh 服务同时暴露在 v6 网络上。查了半天才发现问题。如果你不想处理 IPv6最简单的办法是把 IPv6 的 INPUT/FORWARD 策略也改成 DROP或者直接用 sysctl 关掉 IPv6。但如果你需要 IPv6 提供服务那务必同时管理 /etc/systemd/system/iptables-restore.service 里的 ip6tables-restore 加载项。# 保存 IPv6 规则 ip6tables-save /etc/iptables/rules.v65.5 规则文件换行符与权限问题这个说起来很基础但翻车概率不低。在 Windows 上编辑过规则文件再传到 Linux 上文件里的 CRLF 换行符会让 iptables-restore 解析出错而且报错信息往往不是你一眼能看明白的。我当时遇到的是line 3 failed完全看不出来问题直到拿 hexdump 一看才发现是\r\n的问题。另外一个常见问题是规则文件权限。/etc/sysconfig/iptables 如果权限是 644root 以外的人也能读规则内容就相当于把防火墙策略暴露给了所有能登录系统的用户。虽然没有写权限不至于被篡改但信息泄露本身就已经是风险了。我一般会把权限收紧到 600chmod 600 /etc/sysconfig/iptables顺便说一句iptables-save 重定向到文件时用的是当前 shell 的 umask如果 umask 是 022生成的文件天然是 644。所以每次保存完都养成了顺手 chmod 600 的习惯。6. 复杂规则集怎么管理把保存升级成版本化脚本管理6.1 为什么单一规则文件不够用当服务器上的业务越来越多规则文件会膨胀到几百行。几百行也还算好真正麻烦的是你没法一眼看出每一条规则的来源和用途。三个月之后回来看自己的规则文件你想改一行都不知道动了这一行会不会影响别的业务。单一规则文件还有一个问题改错了很难回滚。虽然可以用备份文件恢复但如果你忘了备份就只能靠记忆重新搭一套。这个记忆是不可靠的特别是在你同时维护多台服务器的时候。所以到后面我会把一套规则拆成一块一块的脚本模块。比如 base.sh 管基础策略web.sh 管 Web 服务端口db.sh 管数据库端口每个模块独立维护最后用一个 master 脚本按顺序组装输出到一份完整的规则文件再加载。其实思路跟写程序很像——把大函数拆成小函数各自负责一件事最后再编排调用。6.2 用函数封装规则块这里我分享一个简化版的脚本模板核心思路是按业务分类生成规则片段再合并成完整规则文件。#!/bin/bash # /usr/local/bin/build-iptables-rules.sh RULES_FILE/etc/iptables-rules.conf # 生成基础模块 gen_base() { cat EOF *filter :INPUT DROP [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] -A INPUT -i lo -j ACCEPT -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT EOF } # 生成远程管理模块 gen_remote_mgmt() { cat EOF -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT -A INPUT -p tcp --dport 22 -s 172.16.0.0/12 -j ACCEPT EOF } # 生成业务端口模块 gen_services() { cat EOF -A INPUT -p tcp --dport 80 -j ACCEPT -A INPUT -p tcp --dport 443 -j ACCEPT -A INPUT -p tcp --dport 3306 -s 10.0.0.0/8 -j ACCEPT EOF } # 生成收尾模块 gen_tail() { cat EOF COMMIT EOF } # 输出完整规则到文件 { gen_base gen_remote_mgmt gen_services gen_tail } $RULES_FILE # 用 test 参数做语法校验 iptables-restore --test $RULES_FILE # 如果校验通过正式加载 if [ $? -eq 0 ]; then iptables-restore $RULES_FILE echo rules loaded from $RULES_FILE else echo syntax error in rules file, abort exit 1 fi这个脚本的运行结果是生成一份完整的规则文件并加载。你也可以把生成规则、语法检查、加载分成三步执行中间插入人工确认环节。好处是每块规则都有独立的函数体来源和用途清清楚楚改某一业务时只需动对应的函数不会影响全局。6.3 把规则文件放进 Git既然规则文件已经是纯文本且格式清晰把它纳入版本管理就是水到渠成的事。我通常会把生成的规则文件放在一个独立的 Git 仓库里修改前先 commit 当前版本改完再 commit 一次commit message 里写清楚改了哪条业务线的规则。这样万一改出问题git revert 就能秒回退比靠备份文件手动恢复靠谱得多。另外我自己还有一个小习惯在规则文件的头部加注释写明这台服务器的角色、最近一次修改的日期和原因。iptables-save 文件本身也能带注释行以 # 开头但它默认不写业务含义。手动加注释有两个好处一是下次看文件时能快速回忆当时的上下文二是其他同事接手这台机器时不至于一脸懵。要注意的是如果你用 iptables-save 重定向覆盖文件覆盖后文件里的自定义注释会被抹掉。所以我会在脚本里专门加一段用注释重新写进去或者更省事一点脚本生成文件后再 sed 插一行注释头。6.4 定时任务主动同步有一种场景会倒逼你加一道保险丝有人手动执行了 iptables 命令改了规则但没有执行保存。重启后规则回滚到旧版那个人还以为是系统出问题了。为了避免这类问题我会配一个 cron 任务每隔一段时间把当前规则自动导出到约定路径# 每天凌晨 3 点自动备份当前规则 0 3 * * * /usr/sbin/iptables-save /root/iptables-backup/iptables.$(date \%F).rules这个定时备份不算保存规则它的定位是快照。真正开机加载用的还是规则脚本构建出来的那份文件。快照的作用是提供时间维度上的回滚点——万一规则被改出了问题你至少能找到昨天的快照来查找差异。不过这里要提醒一句别把定时自动保存当成规则管理的正路。如果每次都自动把当前状态固化成文件而当前状态本身是被临时调试搞乱的那你只是把错误状态持久化而已。自动快照适合做灾备不适合做规范管理。规范的规则集还是应该来源于脚本模板模板变才是真正的变更。7. 最后再分享两个小技巧一个是保存前别忘了带着 -n 参数看一眼规则。iptables -L 默认会做 DNS 反解规则多的时候特别慢而且反解结果不稳定会导致显示混乱。用 iptables -L -n -v 可以禁用反解、显示更明确的信息加 -v 还能看到每个链统计的包数判断规则是否真的被命中过这对排查规则是不是白写了非常有用。另一个是当你用 iptables-save 导出 nat 表的规则时注意 masquerade 规则引用的网卡名特别容易因为命名规则变化而出问题。在云服务器上这个网卡名通常对应主网卡eth0 或者 ens5一旦云平台迁移、网卡重建名字可能就变了。写脚本时尽量把网卡名定义成变量放在脚本头部统一管理换成新环境时只改一处就行。我在实际运维中的体会是iptables 规则保存这件事技术上不复杂但它考验的是流程习惯。规则丢失的锅从来不是 iptables 的错而是没有建立一套变更-测试-保存-验证-记录的闭环。把规则文件当作代码来管理把保存步骤当成发布流程来对待这事就没那么多坑了。