SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

Rocky和AlmaLinux VPS如何配置firewalld防火墙

学习在Rocky或AlmaLinux VPS上用firewalld开放SSH和Web端口、关闭端口并在重启后保留规则,理解区域及--permanent只修改配置文件的陷阱。

firewalld 是什么,以及 Rocky 和 AlmaLinux 为什么默认提供它

firewalld 是 Rocky Linux、AlmaLinux 以及其他 Red Hat Enterprise Linux (RHEL) 重构发行版默认安装的防火墙管理器。它不会自行检查数据包,而是保存配置,并将配置转换为 nftables 规则。使用 firewall-cmd 命令可以在服务器保持在线的情况下修改配置。

如果您已经了解 Ubuntu VPS 上 ufw 的工作方式,就了解这项工作的基本原理。firewalld 增加了 ufw 没有的两个概念。第一个是区域(zones):一种命名策略,数据包会被分类到其中。第二个是运行时规则与已保存规则的分离,这由 --permanent 标志控制,也是该工具最容易引起混淆的地方。

以下所有内容都是在您自己的服务器上运行的命令。请从第二台机器测试每项更改,因为在服务器上看似正确的规则,从互联网访问时仍可能不正确。

首先开放 SSH

大多数 Rocky 和 AlmaLinux 安装都已安装并运行 firewalld,随附配置也允许 SSH。部分精简型云镜像会移除 firewalld。请先检查,不要想当然。

sudo dnf install -y firewalld
sudo systemctl enable --now firewalld
sudo firewall-cmd --state

firewall-cmd --state 会输出 running。如果服务已停止,其他所有 firewall-cmd 调用都会返回 FirewallD is not running,并以非零状态退出。命令看起来完全没有任何作用时,首先应检查这一点。

现在查看当前允许的流量。

sudo firewall-cmd --list-all

实际输出还会多出几行。需要关注的是以下内容:

public (active)
  target: default
  interfaces: eth0
  sources:
  services: cockpit dhcpv6-client ssh
  ports:
  rich rules:

services: 行中的 ssh 是当前会话仍能正常工作的原因。如果缺少该项,请先添加它,再进行其他操作。启动不含 SSH 规则的防火墙会终止当前会话,并且无法重新连接。

sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload

target: default 表示:不匹配任何规则的数据包会被拒绝,并返回 ICMP(互联网控制消息协议)host-prohibited 响应,因此客户端访问关闭端口时会立即看到 No route to host。将目标设置为 DROP 后,服务器会保持静默,扫描器只能等待超时。

sudo firewall-cmd --permanent --zone=public --set-target=DROP
sudo firewall-cmd --reload

执行该操作前,请了解其影响:DROP 还会阻止服务器响应 ping,因此您自己的监控也会失去响应。

为什么我的规则消失了?--permanent 标志

firewalld 同时维护两套配置。运行时配置是内核当前实际执行的配置。永久配置保存在 /etc/firewalld/zones/public.xml 中,并会在 reload 或重启后恢复。

不带 --permanent 的命令只修改运行时配置。它会立即生效,但在下一次 reload 或启动时消失。带 --permanent 的命令会写入文件,但不会修改当前运行中的配置,因此端口在 reload 前仍处于关闭状态。这两种行为都不是错误。但由于无论哪种情况,命令都会输出 success,因此很容易让人误判。

每次都同时写入这两种配置。

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload

您可以分别读取这两套配置,这是最快判断自己犯了哪种错误的方法。

sudo firewall-cmd --list-services
sudo firewall-cmd --permanent --list-services

第一条命令输出当前生效的规则集。第二条命令输出已保存的规则集。如果当前规则集包含某项服务,而已保存的规则集不包含,那么该规则会在下一次 reload 时消失。如果已保存的规则集包含某项服务,而当前规则集不包含,说明您忘记了 reload。sudo firewall-cmd --runtime-to-permanent 会将当前所有配置复制到已保存的文件中,适合在完成一轮测试后使用。

--reload 会保留连接跟踪状态,因此您的 SSH 会话可以继续保持。--complete-reload 还会重新加载内核模块并丢失该状态,通常会终止所有打开的连接,包括您自己的连接。请使用普通的 reload。

系统内置了一种安全机制。运行时规则可以自动过期。

sudo firewall-cmd --add-service=http --timeout=5m

该规则会在五分钟后自行删除。它不能与 --permanent 组合使用,这正是其用途:用于测试您不确定是否安全的更改。更可靠的安全措施是保留第二个 SSH 会话。编辑规则时不要关闭该会话,直到通过新的登录确认新规则正常工作。

区域,以及为什么 VPS 上通常只需关注默认区域

区域是一组带有信任级别的命名权限集合。firewalld 会将每个入站数据包准确地分配到一个区域。它首先将数据包的源地址与各区域的 sources: 列表进行匹配。如果没有匹配项,则使用入站接口所绑定的区域。如果接口未绑定任何区域,数据包就会进入默认区域。

sudo firewall-cmd --get-default-zone
sudo firewall-cmd --get-active-zones

在只有一个网络接口的 VPS 上,第一条命令的结果几乎总是 public,这也是你唯一需要使用的区域。firewall-cmd 不带 --zone= 参数时会作用于默认区域,因此本指南中的所有简短命令都无需指定区域即可生效。

下面是最容易浪费一下午时间的问题。如果接口绑定到了其他区域,你添加的规则会进入 public,但流量却由另一个区域处理。因此,新增的规则不会产生任何效果,也不会收到任何提示。--get-active-zones 可以显示接口绑定情况:

public
  interfaces: eth0

如果接口显示在其他区域名下,可以使用 --zone= 在该区域中编写规则,也可以移动接口。

sudo firewall-cmd --permanent --zone=public --change-interface=eth0
sudo firewall-cmd --reload

Rocky 和 AlmaLinux 使用 NetworkManager 管理网络接口。连接启动时,NetworkManager 会重新应用区域设置。因此也要在 NetworkManager 中设置该区域,避免重启后配置被撤销。连接名称应取自第一条命令的输出,因为它通常与设备名称不同。

sudo nmcli connection show
sudo nmcli connection modify "System eth0" connection.zone public

源地址匹配优先于接口匹配,因此可以为某个地址应用不同的策略。内置的 trusted 区域允许所有流量。

sudo firewall-cmd --permanent --zone=trusted --add-source=203.0.113.10/32
sudo firewall-cmd --reload

使用该区域时必须谨慎。它会向该地址开放服务器上的所有端口,包括你以为只对内部开放的数据库端口。如果只想开放一个端口,而不是信任整台主机,请使用 rich rule。

什么是 firewalld 服务?

服务是一个有名称的端口集合,以 XML 文件形式发布。--add-service=https 会开放 443/tcp,因为 /usr/lib/firewalld/services/https.xml 定义了 https 的含义。

sudo firewall-cmd --get-services
sudo firewall-cmd --info-service=https

--info-service 会列出该名称对应的端口:

https
  ports: 443/tcp

如果已有对应名称,请使用名称。六个月后再次查看 --list-all 时,这样更易读;Cockpit 等软件包也会安装自己的服务文件。对于没有定义的端口,请使用 --add-port

需要注意的差异是:ssh 服务表示 22/tcp,不包含其他端口。如果您在加固服务器上的 SSH 访问时将 SSH 移到了其他端口,那么 --add-service=ssh 不会开放您实际使用的端口。

sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload

在 RHEL 重装系统中,该端口还受到第二层限制。SELinux(security-enhanced Linux)会为端口号设置标签,sshd 不能绑定到标签范围之外的端口。服务随后会拒绝启动,日志会显示 error: Bind to port 2222 on 0.0.0.0 failed: Permission denied.。请先为该端口设置标签。

sudo dnf install -y policycoreutils-python-utils
sudo semanage port -a -t ssh_port_t -p tcp 2222

如何查看当前开放的端口?

sudo firewall-cmd --list-all
sudo firewall-cmd --list-rich-rules
sudo nft list table inet firewalld | head -n 40

前两条命令报告 firewalld 认为已开放的端口。第三条命令读取内核实际保存的规则,这些规则位于 firewalld 管理的表中。三者应保持一致。

但这些信息都不能作为证明。请从另一台计算机进行测试:

nc -zv 203.0.113.20 443

不要在服务器本机运行测试。firewalld 会接受通过 loopback 接口到达的所有流量,因此无论规则如何,curl http://localhost:8080 都会成功。该测试只能说明服务正在运行,不能说明防火墙状态。

放行 Web 端口

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-services

现在,上一条命令列出的内容中应会包含 http https。如果网站仍然无法访问,问题可能不在防火墙。规则只允许数据包通过。仍然必须有进程监听该端口。

sudo ss -tlnp

显示为 0.0.0.0:443*:443 的套接字接受来自任意地址的连接。显示为 127.0.0.1:443 的套接字仅在 loopback 上响应,任何防火墙规则都无法让它从外部访问。Linux 上的端口和监听套接字详细介绍了这一差异。

如何再次关闭端口?

sudo firewall-cmd --permanent --remove-service=http
sudo firewall-cmd --reload

这里同样适用 --permanent 规则,而且在这个方向上影响更严重。只从运行时配置中移除服务后,端口看起来已关闭,但下次重新加载配置或重启时,系统会根据保存的文件重新开放该端口。这会形成一个你无法察觉的漏洞,因为你执行的检查已经通过。

移除原本不存在的内容时,系统会输出 Warning: NOT_ENABLED: http,但退出状态仍为 0。重复添加相同内容时,系统会输出 Warning: ALREADY_ENABLED: http。这两种情况都是安全的。拼写错误的名称则不同:Error: INVALID_SERVICE 表示 firewalld 没有该名称的定义,配置完全没有更改。

如果你的 --list-all 显示 cockpit,并且不使用 9090 端口上的 Cockpit Web 控制台,请将其移除。每个开放端口对应一个需要持续修补的服务。

将端口限制为单个源地址

Rich rules 是完整形式,适用于普通服务名称无法表达所需规则的情况。将 SSH 限制为一个办公地址需要执行两条命令,第二条命令最容易被忽略。

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.10/32" service name="ssh" accept'
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --reload

区域是一组权限,不是按编号排列、匹配到第一条就停止的列表。Rich rule 为一个地址添加允许规则,但不会拒绝其他地址。当 ssh 仍位于 services: 行中时,整个互联网仍可访问端口 22,rich rule 不会产生任何可观察的变化。删除宽泛的条目,否则细化规则只是装饰。

对于没有服务名称的端口,请直接指定端口。

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.10/32" port port="5432" protocol="tcp" accept'

要丢弃产生大量噪声的网络并保留记录,请将 log 元素放在 action 之前,这是 rich rule 语言要求的顺序。

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="198.51.100.0/24" log prefix="fw-drop " level="info" limit value="3/m" drop'
sudo firewall-cmd --reload
sudo journalctl -k -g fw-drop

limit 值可以防止大量数据包填满 journal。将 SSH 锁定到单个地址前,请确认该地址稳定。家庭网络连接的 IP 地址可能会变化,地址一旦变化就会导致您无法登录,因此应先测试并确认能够使用服务提供商的控制台访问。

ufw 命令及其对应的 firewall-cmd 命令

工具不同,任务相同。每一行 --permanent 后都需要一个 sudo firewall-cmd --reload,而这是此类列表无法体现的一点。

  • sudo ufw enable 变为 sudo systemctl enable --now firewalld
  • sudo ufw disable 变为 sudo systemctl disable --now firewalld
  • sudo ufw status verbose 变为 sudo firewall-cmd --list-all
  • sudo ufw allow OpenSSH 变为 sudo firewall-cmd --permanent --add-service=ssh
  • sudo ufw allow 443/tcp 变为 sudo firewall-cmd --permanent --add-port=443/tcp
  • sudo ufw delete allow 443/tcp 变为 sudo firewall-cmd --permanent --remove-port=443/tcp
  • sudo ufw allow from 203.0.113.10 to any port 22 变为上面所示的 rich rule
  • sudo ufw reload 变为 sudo firewall-cmd --reload
  • sudo ufw default deny incoming 已经是 public zone 的行为,--set-target=DROP 则是它的静默版本
  • sudo ufw logging on 变为 sudo firewall-cmd --set-log-denied=all

有一个差异需要明确说明。ufw 会维护编号列表,您可以将规则插入位置 1。firewalld 没有规则编号,因此“将此规则放在第一条”在这里没有意义。当两个 firewalld 条目看起来相互矛盾时,范围更广的允许规则会生效,因为规则集中没有拒绝规则。您需要自行删除范围更广的条目。

为什么防火墙看起来已关闭,但我的 Docker 容器仍可访问?

因为发布的容器端口不会经过您所在区域控制的防火墙部分。docker run -d -p 8080:80 nginx 会让 Docker 写入自己的 NAT(网络地址转换)和转发规则。到达 8080 的数据包会被改写并继续路由到容器,因此会被转发,而不是交付给主机。区域中的 services:ports: 规则控制的是交付给主机的数据包。Docker 的规则控制转发路径,并允许这些数据包通过。

结果是,服务器上的 sudo firewall-cmd --list-all 看不到 8080 端口,但另一台计算机上的 nc -zv 203.0.113.20 8080 仍然可以连接。查看 Docker 安装的规则:

sudo iptables -t nat -L DOCKER -n

修复方法是修改发布参数。将端口绑定到 loopback,再在前面放置反向代理。

docker run -d -p 127.0.0.1:8080:80 nginx

现在,容器只会在服务器上通过 curl http://127.0.0.1:8080 响应,外部无法访问。Ubuntu 用户也会遇到同样的问题,详见为什么 Docker 容器会绕过 ufw 直接发布端口。Rocky 和 AlmaLinux 在基础软件仓库中提供的 rootful Podman,也使用相同的 NAT 方法发布端口。因此,应从另一台计算机进行测试,不要只相信区域列表。

让防火墙在重启后继续生效,以及您会看到的错误

sudo systemctl is-enabled firewalld
sudo systemctl status firewalld

enabledactive (running) 正是您需要的配置。防火墙正在运行但未启用时,只能保护服务器到第一次重启。您应将此项检查加入 新 VPS 上线后前 10 分钟内的检查清单,并与 SSH 密钥和更新一起完成。

原始 nftables 命令与 firewalld 不能混用。 firewalld 管理名为 inet firewalld 的表。sudo nft flush ruleset 会删除该表,此时服务器将对所有流量开放;但 firewall-cmd --list-all 仍会显示您预期的配置,因为 firewalld 报告的是它认为的状态,而不是内核实际保存的规则。sudo firewall-cmd --reload 会重新安装规则。请使用 firewall-cmd 编写规则,以便规则在重新加载后恢复。

在同一台服务器上运行两个防火墙管理器。 在 firewalld 旁安装 ufw 或 iptables-services,会让两个程序在互不了解的情况下写入规则,最终生效的规则取决于哪个服务最后启动。请选择一个。在 Rocky 和 AlmaLinux 上,firewalld 获得发行版支持。

服务器前面的云服务商防火墙。 许多 VPS 控制面板都有独立的网络防火墙。如果 --list-all 显示端口已开放,但从外部建立连接仍然失败,请先检查控制面板,再修改服务器上的配置。反过来也一样:即使控制面板中的规则允许访问,firewalld 拒绝数据包时也不会生效。

不使用 sudo 运行 firewall-cmd。 每项更改都需要 root 权限。没有该权限时,请求会在授权检查阶段被拒绝,任何配置都不会修改;乍看之下,这很像命令被忽略了。

大多数情况下,以下 6 个命令就足够了:使用 --list-all 查看状态,使用 --permanent --add-service--add-port 开放端口,使用 --permanent --remove-service 关闭端口,使用 --reload 应用已保存的文件,并在完成一轮实验后运行 --runtime-to-permanent。区域是 public,标志是 --permanent;唯一可靠的检查方式,是从另一台机器进行验证。

FAQ

我的 firewalld 规则为什么在重启后消失了?

规则只写入了运行时配置。sudo firewall-cmd --add-service=http 会立即生效,并在下次重新加载或启动时丢失,因为从未修改 /etc/firewalld/zones/public.xml 中保存的配置。添加 --permanent,然后运行 sudo firewall-cmd --reload。如果要保留之前手动添加的规则,请运行 sudo firewall-cmd --runtime-to-permanent,将当前生效的规则集复制到保存文件中。

为什么使用 --permanent 添加规则后没有任何变化?

因为 --permanent 只写入文件,不会修改正在运行的防火墙。除非 sudo firewall-cmd --reload 将保存的配置加载到内核,否则端口仍处于关闭状态。比较 sudo firewall-cmd --list-servicessudo firewall-cmd --permanent --list-services:如果保存的列表中有一项,而当前生效的列表中没有,则缺少的就是重新加载操作。

我应该使用 --add-service 还是 --add-port?

对于正在运行的服务,如果存在对应的名称,请使用 --add-service。这样可以明确表达配置意图,sudo firewall-cmd --info-service=https 会显示该名称涵盖的具体端口。如果没有定义对应的服务,或服务监听非标准端口,请使用 --add-portssh 服务仅表示 22/tcp,因此 SSH 改用 2222 端口时,需要使用 --add-port=2222/tcp,并为该端口设置 SELinux 标签。

为什么 firewalld 显示端口已关闭,但 Docker 容器仍可访问?

Docker 会通过自身的 NAT 规则改写已发布的端口,并将流量转发到容器。因此,数据包不会交付给主机,而区域的服务和端口列表只适用于交付给主机的数据包。即使 --list-all 没有显示任何内容,容器仍可从互联网访问。请改为发布到回环地址,使用 docker run -d -p 127.0.0.1:8080:80 nginx,并在其前面配置反向代理。

我可以在 Rocky Linux 上安装 ufw 来替代 firewalld 吗?

同一台服务器上运行两个防火墙管理工具时,它们会在不了解对方配置的情况下写入规则。最终保留哪一组规则,取决于哪个服务最后启动。firewalld 是 Rocky Linux 和 AlmaLinux 支持的工具,系统已预装,并且使用与 ufw 相同的 nftables 后端。只需了解默认区域和 --permanent 标志,就掌握了整个工具。