防火墙规则即代码:迁移中的陷阱

将主机防火墙规则迁移到新机制时,规则可能在清理过程中悄无声息地丢失。这是如何发生的,以及哪种顺序是安全的。

Linux 服务器上的主机防火墙越来越像应用代码一样被对待:规则存放在代码仓库中,变更要经过评审,由工具负责推送。这是正确的——但在切换时,它会设下一个很容易被忽视的陷阱。最终的结果是一台表现完全正常、却什么也不再过滤的服务器。

本文以当今 Linux 发行版的包过滤框架 nftables 为例解释这一机制,描述一种安全的迁移顺序,并补充另外两个易错点:从开放套接字推导出来的规则,以及绕过主机防火墙的容器端口。

发行版服务如何工作

Debian 随 nftables 软件包提供了一个 systemd 服务 nftables.service。它的 Unit 文件简短而明确:启动和重新加载时,它用 nft -f 加载文件 /etc/nftables.conf。停止时,它执行 nft flush ruleset。

此外还有随附的配置文件,其第一条指令就是 flush ruleset。nftables Wiki 描述了这条命令的作用:它删除完整的规则集——“flush (erase, delete, wipe) the complete ruleset”(清空、擦除、删除、抹去完整的规则集)。这里指的不仅是该服务创建的那些表,而是内核中加载的一切。

对于一台只有这个服务在管理规则的服务器来说,这是一种合理的设计:每次加载时规则集都被完整替换,不会残留早先状态的遗留物。

切换机制时的陷阱

一旦加入第二种机制,问题就来了。一个典型的经过:规则今后应来自代码仓库,并通过配置管理或一个自有服务加载到一张单独的表中。新规则集被加载、检查,并被认为没有问题。旧服务则继续保留,因为它看上去并不碍事。

此后某个时候,会发生以下三件事之一:

  • 有人停止了旧服务,例如在清理时。停止操作会清空整个规则集——包括那张新表。此后主机便处于未过滤状态。
  • 有人重启或重新加载了它。 旧文件被加载,其中的 flush ruleset 会删除一切不在该文件中的内容。主机又回到了原本已被取代的那个状态。
  • 有人手动加载了旧文件,为了“快速测试一下”——结果相同。

在任何一种情况下都不会出现错误信息。服务依然可达,应用正常运行,监控保持绿色。这正是这个陷阱的危险所在:变更后的常规测试检查的是某样东西是否能工作——而不是某样东西是否被拦截。

安全的顺序

解决方法不是一项新技术,而是一个顺序:

  1. 停用或屏蔽(mask)旧服务,但不要停止它。 systemctl 手册页在这一点上说得很清楚:disable 不会停止 Unit;只有加上选项 --now 才会。mask 更进一步,使任何启动都不可能,包括手动启动。而 stop 恰恰就是那条会清空规则集的命令。
  2. 加载新规则集——放在单独的表中,来自代码仓库,通过今后负责管理它的机制加载。
  3. 读出实际加载的内容。 nft list tables 显示存在哪些表;nft list ruleset 显示完整状态。在这一阶段,旧表和新表应当并存。两者会同时生效——请让这一阶段尽量短。
  4. 从外部证明过滤有效——下文详述。
  5. 然后再有针对性地删除旧表,使用 nft delete table,而不是通过停止旧服务。旧配置文件应予归档,或修改为即使被误加载也不再清空任何内容。
  6. 下次重启之后再次读出,确认新规则集已加载、旧规则集没有回来。

有一个细节值得注意:停用和屏蔽会阻止再次启动,但不会结束当前的运行状态。由于该 Unit 被设置为带有 RemainAfterExit=yes 的一次性服务,在 systemd 看来它会一直显示为活动状态,直到下次重启——而它的停止命令也一直保留在那里。请将此记录为一项已知风险。

从外部进行反向探测

每次防火墙变更之后,都会检查允许的服务是否可达。这证明了没有东西坏掉。但它证明不了过滤在起作用——一台完全没有规则的主机同样能通过这项测试。

因此,每一次变更都应包含一个反向探测:从主机之外一个必须被拦截的来源,向一个必须被封锁的端口发起连接尝试。只有当这次尝试失败时,效果才算得到证明。“之外”要按字面理解:主机与自身建立的连接经由回环接口,而规则集通常会对其一概放行。

请定期重复这项探测,而不只是在变更时:上述陷阱可能要到数周之后才会触发。

规则来自代码仓库,而不是来自 Shell

控制台上的临时命令,天然是一切“规则即代码”管理方式的对立面。一条只存在于运行中内核里的规则,会在下一次从代码仓库加载时消失——或者在没人预料到的地方存活下来,因为有人把运行中的状态导出到了一个文件里。两者都会让规则集变得不可预测。规则很简单:凡是不在经过审查的代码仓库中的东西,就不应出现在主机上。紧急变更要事后补录,而定期比对代码仓库与 nft list ruleset 可以让偏差显现出来。

并非每个开放的套接字都需要一条规则

在构建新规则集时,人们很自然地会逐一处理主机的开放套接字列表,并为每一个创建一条放行规则。这会导致多余的开放。一个例子是日志服务仅用于发送的 UDP 套接字,例如在将 Syslog 消息转发到中央服务器时。它会出现在套接字列表中,但并不需要入站规则。

请从用途出发推导放行规则:这台主机应当接受来自谁的哪些连接?套接字列表是用于核对的工具,而不是模板。

容器端口绕过 INPUT

第三个易错点涉及运行 Docker 的主机。根据 Docker 文档,已发布的端口默认绑定到主机的所有地址;端口发布是“insecure by default”(默认不安全)的。数据包会经过目标地址 NAT 重定向,并在 FORWARD 链中被转交给 Docker 自有的链。对于 UFW,文档明确写出了其后果:Docker 在 nat 表中对容器流量进行重定向,此时流量尚未到达 UFW 所使用的 INPUT 和 OUTPUT 链。因此,一条精心维护的 INPUT 规则看不到已发布的容器端口。

如果容器中的某个服务只应在本地可达,请将其明确绑定到 127.0.0.1,例如使用 -p 127.0.0.1:8080:80。根据文档,这样就只有主机本身可以访问——但有一个限制:在 Docker 28.0.0 之前,同一二层网段中的主机也能访问发布在 localhost 上的端口。对于也应在容器之前生效的过滤规则:使用默认的 iptables 后端时,Docker 为此提供了 DOCKER-USER 链;使用实验性的 nftables 后端时,则没有这条链。此外,已发布的容器端口也应纳入每一次反向探测。

sector7 如何提供支持

我们对操作系统和标准服务进行加固,并与您一起建立清晰、可追溯的规则体系——详情请见网络安全。定期的自动化漏洞扫描会从外部检查您的攻击面,并在内部检查系统和服务;当某个本不应开放的端口处于开放状态时,扫描会将其显示出来。在我们的托管网络服务框架内,我们也承担变更管理。

来源

让我们聊聊您的具体情况。