Linux 实战(二):端口不通 —— CentOS 7、Rocky 9、Ubuntu 24.04 上防火墙和 SELinux 的实测差异

文章来源声明: 原文作者:xiaoqiMikko; 来源站点:掘金; 原文链接:https://juejin.cn/post/7694480437335244815; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

这篇把端口不通的排查链条讲透了,尤其区分 firewalld 与 ufw 的报错差异、iptables 顺序和 SELinux 持久化,适合运维排障时按图索骥,避免在防火墙与 SELinux 之间瞎折腾。

本文目录
这一篇讲什么 ------

「端口不通」是最常见也最容易瞎折腾的问题:服务起了、防火墙也开了,外面还是连不上。本篇在三台机器上把排查链条从头到尾实跑一遍:先分清报错类型 → 本机在不在监听 → 防火墙 → SELinux / AppArmor。

实测环境同(一):CentOS 7.9(VMware)、Rocky 9.8、Ubuntu 24.04.5(KVM)。本篇额外装了 nginx、nmap、policycoreutils-python-utils、setroubleshoot-server、mysql-server。所有「从外面连」的测试都是从另一台机器发起的:测 Rocky / CentOS 时从 Ubuntu 连,测 Ubuntu 时从 Rocky 连。


  1. 先看报错长什么样 —— 三种报错对应三件不同的事 ✅

从客户端连一个端口,会看到三种结果之一。实测里它们和原因的对应关系,和很多文章说的不一样:

客户端看到实测在什么情况下出现
`Connection refused`(立刻返回)包到了机器,但那个端口没人监听;或服务只监听了 `127.0.0.1`;或 ufw 的 `limit` 规则触发
`No route to host`(立刻返回)**被 firewalld 挡了**(Rocky 9 和 CentOS 7 的默认配置都是这样)
超时(等满才返回)**被 ufw 挡了**(Ubuntu 默认策略)

🔴 「超时 = 被防火墙丢了、拒绝 = 没人监听」这条经验,在 firewalld 上不成立。 firewalld 的默认 zone 是用 REJECT 回一个 ICMP 包,客户端立刻看到 No route to host。下面是原始输出。

1.1 被 firewalld 挡:No route to host

Rocky 9 上起一个监听 8080 的服务,防火墙没放行:

# ss -lntp | grep 8080          (Rocky 9)
LISTEN 0      5            0.0.0.0:8080      0.0.0.0:*    users:(("python3",pid=909,fd=3))

从另一台机器连:

$ nc -zv -w 3 10.115.180.175 8080
nc: connect to 10.115.180.175 port 8080 (tcp) failed: No route to host

$ curl -sS -m 5 -o /dev/null http://10.115.180.175:8080/
curl: (7) Failed to connect to 10.115.180.175 port 8080 after 0 ms: Couldn't connect to server

$ nmap -Pn -p 8080,9999 10.115.180.175
8080/tcp filtered http-proxy
9999/tcp filtered abyss

注意 9999 端口根本没人监听,结果和 8080 一模一样:

$ nc -zv -w 3 10.115.180.175 9999
nc: connect to 10.115.180.175 port 9999 (tcp) failed: No route to host

⚠️ 也就是说,端口被 firewalld 挡住时,你从外面看不出后面有没有服务在监听。要先在本机 ss -lntp 确认服务真的起来了。

CentOS 7 同样是 No route to host,原因在 INPUT 链最后一条:

# iptables -L INPUT -n --line-numbers          (CentOS 7,截取)
6    DROP       all  --  0.0.0.0/0            0.0.0.0/0            ctstate INVALID
7    REJECT     all  --  0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited

1.2 被 ufw 挡:超时

Ubuntu 上 ufw enable 之后(只放行了 22),从 Rocky 连 8080:

$ start=$(date +%s); nc -zv -w 5 10.115.180.200 8080; echo "nc 退出码=$? 用时=$(( $(date +%s)-start ))s"
Ncat: TIMEOUT.
nc 退出码=1 用时=5s

$ curl -sS -m 5 -o /dev/null http://10.115.180.200:8080/
curl: (28) Connection timed out after 5002 milliseconds

$ nmap -Pn -p 8080,9999 10.115.180.200
8080/tcp filtered http-proxy
9999/tcp filtered abyss

nmap 两种情况都显示 filtered,但 nc / curl 的表现完全不同。看 nc/curl 的报错能多知道一件事:挡你的大概率是哪一类防火墙。

1.3 端口放行了但没人监听:Connection refused

Rocky 上放行 9999,但不起服务:

$ nc -zv -w 3 10.115.180.175 9999
nc: connect to 10.115.180.175 port 9999 (tcp) failed: Connection refused
$ nmap -Pn -p 9999 10.115.180.175
9999/tcp closed abyss

1.4 服务只监听 127.0.0.1:从外面看也是 Connection refused

# ss -lntp | grep -E ':808[0-2]'          (Rocky 9)
LISTEN 0      5            0.0.0.0:8081      0.0.0.0:*    users:(("python3",pid=1009,fd=3))
LISTEN 0      5            0.0.0.0:8080      0.0.0.0:*    users:(("python3",pid=909,fd=3))
LISTEN 0      5          127.0.0.1:8082      0.0.0.0:*    users:(("python3",pid=1102,fd=3))
本机 curl 127.0.0.1:8082 → 200

8082 防火墙已经放行,从外面连:

$ nc -zv -w 3 10.115.180.175 8082
nc: connect to 10.115.180.175 port 8082 (tcp) failed: Connection refused
$ nmap -Pn -p 8082 10.115.180.175
8082/tcp closed blackice-alerts

🔑 看 ss -lntp 的第四列:0.0.0.0:端口 才是所有网卡;127.0.0.1:端口 只有本机能连,这种情况改防火墙没用,要改服务的监听地址。


  1. 先确认这台机器用的是哪套防火墙 ✅

CentOS 7Rocky 9Ubuntu 24.04
`firewall-cmd --state``running``running`没有 firewalld
`ufw status`没有 ufw没有 ufw`Status: inactive`(装了但默认没开)
`iptables -V``iptables v1.4.21``iptables v1.8.10 (nf_tables)``iptables v1.8.10 (nf_tables)`
firewalld 后端iptables(配置文件里没有 `FirewallBackend` 这一行)`FirewallBackend=nftables`—
网卡名`ens33``enp5s0``enp5s0`

⚠️ firewall-cmd --state 在 firewalld 没运行时:

# systemctl stop firewalld; firewall-cmd --state; echo "退出码=$?"          (Rocky 9)
not running
退出码=252

脚本里判断时看退出码,别只看输出里有没有 running。


  1. firewalld:--permanent 和 --reload 的两个方向 ✅

3.1 加了 --permanent,不 reload,当前不生效

# firewall-cmd --zone=public --add-port=8080/tcp --permanent
success
# firewall-cmd --zone=public --list-ports

# firewall-cmd --permanent --zone=public --list-ports
8080/tcp

运行时里是空的,只写进了配置。此时从外面连:

$ nc -zv -w 3 10.115.180.175 8080
nc: connect to 10.115.180.175 port 8080 (tcp) failed: No route to host

--reload 之后才通:

# firewall-cmd --reload; firewall-cmd --zone=public --list-ports
success
8080/tcp

$ nc -zv -w 3 10.115.180.175 8080; curl -sS -m 5 -o /dev/null -w '%{http_code}\n' http://10.115.180.175:8080/
Connection to 10.115.180.175 8080 port [tcp/http-alt] succeeded!
200

3.2 不加 --permanent:不用等重启,一次 reload 就没了

# firewall-cmd --add-port=8081/tcp; firewall-cmd --list-ports
success
8080/tcp 8081/tcp

$ nc -zv -w 3 10.115.180.175 8081
Connection to 10.115.180.175 8081 port [tcp/tproxy] succeeded!

# firewall-cmd --reload; firewall-cmd --list-ports
success
8080/tcp

$ nc -zv -w 3 10.115.180.175 8081
nc: connect to 10.115.180.175 port 8081 (tcp) failed: No route to host

🔴 很多文章说「不加 --permanent 重启就没了」—— 实际上任何人执行一次 firewall-cmd --reload,你临时加的规则就没了,不用等到重启。

稳妥写法就是两条连着敲:

firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --reload

3.3 端口段是减号

# firewall-cmd --add-port=8083-8085/tcp; echo "减号 退出码=$?"
success
减号 退出码=0
# firewall-cmd --add-port=8083:8085/tcp; echo "冒号 退出码=$?"
Error: INVALID_PORT: 8083:8085
冒号 退出码=102

ufw 正好反过来(见 §6.3)。

3.4 按服务名开、看网卡在哪个 zone

# firewall-cmd --get-services | wc -w
225
# firewall-cmd --add-service=http; firewall-cmd --list-services
success
cockpit dhcpv6-client http ssh

# firewall-cmd --get-active-zones
public
  interfaces: enp5s0

开端口之前先 --get-active-zones 看网卡在哪个 zone,规则要加在网卡所在的那个 zone 上。

排查时想确认「是不是防火墙的问题」,可以临时把默认 zone 切成 trusted(全放行):

# firewall-cmd --set-default-zone=trusted; firewall-cmd --get-active-zones
success
trusted
  interfaces: enp5s0

$ nc -zv -w 3 10.115.180.175 8081
Connection to 10.115.180.175 8081 port [tcp/tproxy] succeeded!

# firewall-cmd --set-default-zone=public          ← 确认完立刻改回来

3.5 只对某个来源 IP 开放(富规则)

# firewall-cmd --add-rich-rule='rule family="ipv4" source address="10.115.180.200" port protocol="tcp" port="8086" accept'
success
# firewall-cmd --list-rich-rules
rule family="ipv4" source address="10.115.180.200" port port="8086" protocol="tcp" accept

从 10.115.180.200 连能通,从 10.115.180.1 连不通:

$ nc -zv -w 3 10.115.180.175 8086          (从 .200)
Connection to 10.115.180.175 8086 port [tcp/*] succeeded!

$ nc -zv -w 3 10.115.180.175 8086          (从 .1)
nc: connect to 10.115.180.175 port 8086 (tcp) failed: No route to host

整条规则外面用单引号、里面用双引号是标准写法。顺带一提:实测把外层写成双引号也返回 success,存进去的规则一样 —— 那是因为这几个值里都没有空格,bash 拼接完恰好还是合法的。值里带空格时就不是这样了,别依赖这种巧合,照标准写。


  1. 🔴 firewalld 和手工 iptables / nft 规则:7 和 9 表现相反 ✅

4.1 CentOS 7:手工 iptables 规则,一 reload 就没

# iptables -I INPUT -p tcp --dport 8080 -j ACCEPT          (CentOS 7)
$ nc -zv -w 3 192.168.3.100 8080
Connection to 192.168.3.100 8080 port [tcp/http-alt] succeeded!

# firewall-cmd --reload; iptables -S INPUT | grep -c 'dport 8080'
success
0

$ nc -zv -w 3 192.168.3.100 8080
nc: connect to 192.168.3.100 port 8080 (tcp) failed: No route to host

4.2 Rocky 9:手工 iptables 规则,reload 之后还在

# iptables -I INPUT -p tcp --dport 8091 -j ACCEPT; iptables -S | grep 8091          (Rocky 9)
-A INPUT -p tcp -m tcp --dport 8091 -j ACCEPT
# firewall-cmd --reload; echo "iptables 里 8091: $(iptables -S | grep -c 8091)"
success
iptables 里 8091: 1

Rocky 9 的 firewalld 用的是 nftables 后端(FirewallBackend=nftables)。推测原因是它只重建自己那张 nft 表,iptables 命令(nf_tables 版)写的规则在另一张表里,所以 reload 没碰到 —— 这是推测,实测只证明了「reload 后还在」这个现象。

而直接往 firewalld 自己的 nft 表里加规则,连加都加不进去:

# nft add rule inet firewalld filter_IN_public_allow tcp dport 8090 accept
Error: Could not process rule: Operation not permitted
add rule inet firewalld filter_IN_public_allow tcp dport 8090 accept
              ^^^^^^^^^

4.3 Rocky 9 上用 iptables -L 看不到 firewalld 的规则

# iptables -L -n          (Rocky 9,firewalld 正在运行、已放行 8080 和 9999)
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         
---- iptables -S 行数: 3

# nft list ruleset | grep -nE 'dport (8080|8081|9999)'
156:		tcp dport 8080 accept
157:		tcp dport 9999 accept

🔴 Rocky 9 上 iptables -L 空着不代表没有防火墙规则,要看 nft list ruleset 或者直接 firewall-cmd --list-all。CentOS 7 上则相反,iptables -L 能看到 firewalld 生成的全部链(IN_public_allow 等)。

建议:跑着 firewalld 的机器,规则一律用 firewall-cmd 加,别手工敲 iptables / nft —— 它在 7 上会被冲掉、在 9 上会变成 firewalld 看不见的「第二套规则」,两种都会让以后排查的人摸不着头脑。


  1. iptables:-A 追加的规则永远轮不到 ✅

CentOS 7(firewalld 运行中)用 -A 追加一条放行 8080:

# iptables -A INPUT -p tcp --dport 8080 -j ACCEPT; iptables -L INPUT -n --line-numbers          (截取)
6    DROP       all  --  0.0.0.0/0            0.0.0.0/0            ctstate INVALID
7    REJECT     all  --  0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited
8    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8080

$ nc -zv -w 3 192.168.3.100 8080
nc: connect to 192.168.3.100 port 8080 (tcp) failed: No route to host

规则看得见,但不通。-v 的计数器说明了一切:

# iptables -L INPUT -n -v --line-numbers | tail -3
6        1    40 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate INVALID
7        3   164 REJECT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited
8        0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080

iptables 从上往下匹配、命中即停。第 7 条 REJECT 已经把包处理掉了(3 个包),第 8 条 ACCEPT 一个包都没轮到(0)。换成 -I 插到最前面:

# iptables -D INPUT -p tcp --dport 8080 -j ACCEPT; iptables -I INPUT -p tcp --dport 8080 -j ACCEPT
$ nc -zv -w 3 192.168.3.100 8080
Connection to 192.168.3.100 8080 port [tcp/http-alt] succeeded!

# iptables -L INPUT -n -v --line-numbers | head -4
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination         
1        4   216 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080

🔑 某条规则的 pkts 一直是 0,说明包根本没走到它,别再改它了,去看它前面是谁先处理了包。

5.1 CentOS 7 上 service iptables save 存不了

# service iptables save; echo "退出码=$?"          (CentOS 7,未装 iptables-services)
The service command supports only basic LSB actions (start, stop, restart, try-restart, reload, force-reload, status). For other actions, please try to use systemctl.
退出码=2

跑着 firewalld 的 CentOS 7,持久化请走 firewall-cmd --permanent。改规则前想留个底,用 iptables-save:

# iptables-save > /root/iptables.bak.lab; wc -l /root/iptables.bak.lab
175 /root/iptables.bak.lab


  1. ufw(Ubuntu 24.04)✅

6.1 enable 会先问你

# ufw allow 22/tcp; echo n | ufw enable
Rules updated
Rules updated (v6)
Command may disrupt existing ssh connections. Proceed with operation (y|n)? Aborted

# echo y | ufw enable; ufw status verbose
Command may disrupt existing ssh connections. Proceed with operation (y|n)? Firewall is active and enabled on system startup
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere                  
22/tcp (v6)                ALLOW IN    Anywhere (v6)             

写进脚本时用 ufw --force enable 跳过确认。

6.2 🔴 忘了放行 22 就 enable:会怎样

先 ufw --force reset 清空规则(它会先备份):

# ufw --force reset          (截取)
Backing up 'user.rules' to '/etc/ufw/user.rules.20260921_155319'
Backing up 'before.rules' to '/etc/ufw/before.rules.20260921_155319'
...
# ufw --force enable; ufw status verbose
Firewall is active and enabled on system startup
Status: active
Logging: on (medium)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

一条放行规则都没有。从另一台机器新建 SSH 连接:

$ start=$(date +%s); nc -zv -w 5 10.115.180.200 22; echo "新 SSH 连接 退出码=$? 用时=$(( $(date +%s)-start ))s"
Ncat: TIMEOUT.
新 SSH 连接 退出码=1 用时=5s

但 enable 之前就连着的那个 SSH 会话没有断,还能继续敲命令,于是可以自救:

# ufw disable; ufw status          (在原来那个会话里)
Firewall stopped and disabled on system startup
Status: inactive

🔑 所以远程改防火墙的铁律:手上留一个已经连着的会话别关,另开一个新会话测试。新会话连得上,再关旧的。

6.3 开端口:冒号表示端口段

# ufw allow 8081-8083/tcp; echo "减号 退出码=$?"
ERROR: Bad port
减号 退出码=1
# ufw allow 8081:8083/tcp; echo "冒号 退出码=$?"
Rule added
Rule added (v6)
冒号 退出码=0

ufw allow 8080 和 ufw allow 8080/tcp 是两条不同的规则(前者 tcp+udp 都开):

# ufw allow 8080; ufw allow 8080/tcp; ufw status numbered          (截取)
[ 1] 22/tcp                     ALLOW IN    Anywhere                  
[ 2] 8080                       ALLOW IN    Anywhere                  
[ 3] 8080/tcp                   ALLOW IN    Anywhere                  
[ 4] 22/tcp (v6)                ALLOW IN    Anywhere (v6)             
[ 5] 8080 (v6)                  ALLOW IN    Anywhere (v6)             
[ 6] 8080/tcp (v6)              ALLOW IN    Anywhere (v6)             

只允许某个来源:

# ufw allow from 10.115.180.175 to any port 3306 proto tcp
Rule added

6.4 删规则:按编号删,会问你,而且只删 IPv4 那一条

# echo y | ufw delete 2; ufw status numbered
Deleting:
 allow 8080
Proceed with operation (y|n)? Rule deleted
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 22/tcp                     LIMIT IN    Anywhere                  
[ 2] 8081:8083/tcp              ALLOW IN    Anywhere                  
[ 3] 3306/tcp                   ALLOW IN    10.115.180.175            
[ 4] 22/tcp (v6)                LIMIT IN    Anywhere (v6)             
[ 5] 8080 (v6)                  ALLOW IN    Anywhere (v6)             
[ 6] 8081:8083/tcp (v6)         ALLOW IN    Anywhere (v6)             

两点:①删完之后编号重排了,要连删多条就每次重新 status numbered;②**8080 (v6) 还留着** —— 按编号删只删了你指的那一行。按规则原文删会把 v4 和 v6 一起删掉:

# ufw delete allow 8080/tcp
Rule deleted
Rule deleted (v6)

6.5 ufw limit:第 6 次连接被拒

# ufw limit 22/tcp; ufw status | grep 22
Rule updated
Rule updated (v6)
22/tcp                     LIMIT       Anywhere                  
22/tcp (v6)                LIMIT       Anywhere (v6)             

从另一台机器连续连 8 次:

第 1 次: Connected
第 2 次: Connected
第 3 次: Connected
第 4 次: Connected
第 5 次: Connected
第 6 次: refused
第 7 次: refused
第 8 次: refused

被限速拦下的连接是 refused(拒绝),不是超时 —— 和 ufw 默认策略的超时不一样。日志里能看到:

# grep 'UFW' /var/log/ufw.log | tail -1          (截取)
2026-09-21T15:52:09.874603+00:00 ubuntu2404 kernel: [UFW LIMIT BLOCK] IN=enp5s0 OUT= MAC=00:16:3e:47:a0:20:00:16:3e:93:b9:fc:08:00 SRC=10.115.180.175 DST=10.115.180.200 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=33344 DF PROTO=TCP SPT=34

日志在 /var/log/ufw.log(这台机器上存在,属主 syslog:adm)。调日志级别:

# ufw logging medium; ufw status verbose | grep Logging
Logging enabled
Logging: on (medium)

⚠️ ufw logging 后面必须带级别,光敲 ufw logging 会打出帮助和 ERROR: Invalid syntax。

6.6 应用配置

# ufw app list
Available applications:
  OpenSSH

这台机器上只注册了 OpenSSH。


  1. SELinux(Rocky 9 / CentOS 7):防火墙全对还是不通 ✅

两台 RHEL 系机器都是 Enforcing:

# getenforce; sestatus | head -5          (Rocky 9)
Enforcing
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing

7.1 nginx 改到 8888 端口,起不来

# cat /etc/nginx/conf.d/lab.conf
server {
    listen 8888;
    root /usr/share/nginx/html;
    location /api/ { proxy_pass http://127.0.0.1:5000/; }
}
# nginx -t
nginx: configuration file /etc/nginx/nginx.conf test is successful
# systemctl start nginx
Job for nginx.service failed because the control process exited with error code.

nginx -t 通过,启动失败。日志:

# journalctl -u nginx --no-pager | grep -iE 'bind|denied'
Sep 21 15:54:07 rocky9 nginx[2502]: nginx: [emerg] bind() to 0.0.0.0:8888 failed (13: Permission denied)

Permission denied,但 nginx 是 root 启动的 —— 这就是 SELinux 的典型症状。5 秒确认:

# setenforce 0; systemctl start nginx; echo "Permissive 下 start 退出码=$?"; systemctl stop nginx; setenforce 1; getenforce
Permissive 下 start 退出码=0
Enforcing

切到 Permissive 就能起,说明是 SELinux。确认完立刻 setenforce 1 改回去,然后按规则解决。

看它拦了什么:

# ausearch --input-logs -m avc -ts today | grep -m1 'name_bind'
type=AVC msg=audit(1790006047.686:50): avc:  denied  { name_bind } for  pid=2502 comm="nginx" src=8888 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0

装了 setroubleshoot-server 的话,sealert 会直接给修法:

# sealert -a /var/log/audit/audit.log          (截取)
SELinux is preventing /usr/sbin/nginx from name_bind access on the tcp_socket port 8888.

*****  Plugin bind_ports (92.2 confidence) suggests   ************************

If you want to allow /usr/sbin/nginx to bind to network port 8888
Then you need to modify the port type.
Do
# semanage port -a -t PORT_TYPE -p tcp 8888
    where PORT_TYPE is one of the following: http_cache_port_t, http_port_t, jboss_management_port_t, jboss_messaging_port_t, ntop_port_t, puppet_port_t.

7.2 把端口加进 http_port_t

# semanage port -l | grep -E '^(http_port_t|http_cache_port_t|ssh_port_t) '
http_cache_port_t              tcp      8080, 8118, 8123, 10001-10010
http_cache_port_t              udp      3130
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
ssh_port_t                     tcp      22

# semanage port -a -t http_port_t -p tcp 8888; semanage port -l | grep '^http_port_t '
http_port_t                    tcp      8888, 80, 81, 443, 488, 8008, 8009, 8443, 9000

# systemctl start nginx; ss -lntp | grep 8888
LISTEN 0      511          0.0.0.0:8888      0.0.0.0:*    users:(("nginx",pid=2756,fd=6),("nginx",pid=2755,fd=6),("nginx",pid=2754,fd=6))

semanage 在 Rocky 9 上属于 policycoreutils-python-utils,CentOS 7 上属于 policycoreutils-python(这台 CentOS 7 已装)。

🔴 8080 已经被定义成 http_cache_port_t 了,再 -a 加给 http_port_t,两个版本表现不同:

# semanage port -a -t http_port_t -p tcp 8080; echo "-a 退出码=$?"          (CentOS 7)
ValueError: Port tcp/8080 already defined
-a 退出码=1

# semanage port -a -t http_port_t -p tcp 8080; echo "-a 退出码=$?"          (Rocky 9)
Port tcp/8080 already defined, modifying instead
-a 退出码=0

CentOS 7 上要用 -m:

# semanage port -m -t http_port_t -p tcp 8080; echo "-m 退出码=$?"          (CentOS 7)
-m 退出码=0

7.3 nginx 反代报 502,日志只说 Permission denied

后端 127.0.0.1:5000 直连正常,经 nginx 反代就 502:

直连后端 → 200
经 nginx 反代 → 502

# tail -1 /var/log/nginx/error.log          (截取)
2026/09/21 16:04:13 [crit] 2756#2756: *1 connect() to 127.0.0.1:5000 failed (13: Permission denied) while connecting to upstream, client: 127.0.0.1, server: , request: "GET /api/ HTTP/1.1", upstream: "http://127.0.0.1:5000/", host: "127.0.0.1:8888"

nginx 日志里一个字都没提 SELinux。审计日志里是 name_connect:

# ausearch --input-logs -m avc -ts today | grep -m1 'name_connect'
type=AVC msg=audit(1790006653.557:135): avc:  denied  { name_connect } for  pid=2756 comm="nginx" dest=5000 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:commplex_main_port_t:s0 tclass=tcp_socket permissive=0

打开布尔开关:

# getsebool httpd_can_network_connect; setsebool httpd_can_network_connect 1; getsebool httpd_can_network_connect
httpd_can_network_connect --> off
httpd_can_network_connect --> on
经 nginx 反代 → 200

7.4 🔴 setsebool 不加 -P,重启就回去了(重启实测)

不加 -P 时,semanage boolean -l 里「当前值」和「默认值」不一样:

# semanage boolean -l | grep -E '^httpd_can_network_connect '
httpd_can_network_connect      (on   ,  off)  Allow httpd to can network connect

重启后:

# uptime -p; getsebool httpd_can_network_connect
up 1 minute
httpd_can_network_connect --> off

加 -P 再重启:

# setsebool -P httpd_can_network_connect 1; semanage boolean -l | grep -E '^httpd_can_network_connect '
httpd_can_network_connect      (on   ,   on)  Allow httpd to can network connect
(重启)
# uptime -p; getsebool httpd_can_network_connect
up 1 minute
httpd_can_network_connect --> on

和 firewalld 的 --permanent 是同一类坑:当时好了,重启又坏。

7.5 mv 过来的文件 403,cp 过来的正常

# echo hi > /root/mv.html; echo hi > /root/cp.html
# cp /root/cp.html /usr/share/nginx/html/cp.html
# mv /root/mv.html /usr/share/nginx/html/mv.html
# ls -Z /usr/share/nginx/html/cp.html /usr/share/nginx/html/mv.html
unconfined_u:object_r:httpd_sys_content_t:s0 /usr/share/nginx/html/cp.html
       unconfined_u:object_r:admin_home_t:s0 /usr/share/nginx/html/mv.html

cp.html → 200
mv.html → 403

cp 出来的新文件继承目标目录的标签;mv 保留原来在 /root 下的 admin_home_t。ls -l 看权限完全正常,只有 ls -Z 能看出区别。修:

# restorecon -v /usr/share/nginx/html/mv.html
Relabeled /usr/share/nginx/html/mv.html from unconfined_u:object_r:admin_home_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
restorecon 后 mv.html → 200

7.6 网站目录放在 /data/www:chcon 是临时的

/data/www 下的文件标签是 default_t,nginx 读不了。用 chcon 改标签能立刻好:

# chcon -R -t httpd_sys_content_t /data/www; ls -Z /data/www/index.html
unconfined_u:object_r:httpd_sys_content_t:s0 /data/www/index.html
chcon 后 → 200

但只要有人跑一次 restorecon(或者系统重新打标签),就变回去了:

# restorecon -Rv /data/www
Relabeled /data/www from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:default_t:s0
Relabeled /data/www/index.html from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:default_t:s0
restorecon 后 → 403

持久的做法:先 semanage fcontext 注册规则,再 restorecon。只注册不 restorecon 是不生效的:

# semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"; ls -Z /data/www/index.html
unconfined_u:object_r:default_t:s0 /data/www/index.html
只注册未 restorecon → 403

# restorecon -Rv /data/www
Relabeled /data/www from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
Relabeled /data/www/index.html from unconfined_u:object_r:default_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
注册 + restorecon 后 → 200

之后再跑 restorecon,标签也不会变回去了:

# restorecon -Rv /data/www; ls -Z /data/www/index.html
unconfined_u:object_r:httpd_sys_content_t:s0 /data/www/index.html

7.7 ⚠️ 脚本里的 ausearch 会卡住

在远程执行、定时任务这类「有标准输入但不是终端」的环境里,ausearch 会去读标准输入,而不是读审计日志,表现就是一直挂着不返回(本次实测三次都卡到超时)。两种写法都能避免:

# ausearch -m avc -ts today < /dev/null | tail -2          (截取)
type=AVC msg=audit(1790007255.865:215): avc:  denied  { getattr } for  pid=3137 comm="nginx" path="/data/www/index.html" dev="sda2" ino=3324675 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
退出码=0

或者加 --input-logs(上面几段就是这么写的)。

7.8 audit2allow:会把日志里所有的拒绝一起放行

# ausearch --input-logs -m avc -ts today | audit2allow -m labtest          (截取)

module labtest 1.0;

require {
	type admin_home_t;
	type commplex_main_port_t;
	type fs_t;
	type setroubleshootd_t;
	type default_t;
	type httpd_t;
	type unreserved_port_t;
	class tcp_socket { name_bind name_connect };
	class file { getattr read };
	class filesystem getattr;
}

#============= httpd_t ==============
allow httpd_t admin_home_t:file read;

注意第一条就是 allow httpd_t admin_home_t:file read —— 也就是「允许 nginx 读 /root 下来的文件」,这是 §7.5 那次 mv 留下的拒绝记录。audit2allow 不分青红皂白,把今天所有的拒绝都变成了允许。能用 semanage port / setsebool / restorecon 解决的,优先用它们;真要用 audit2allow,先把生成的规则一条条看过。


  1. AppArmor(Ubuntu 24.04)✅

# aa-status | head -3
apparmor module is loaded.
112 profiles are loaded.
18 profiles are in enforce mode.

8.1 nginx 没有 AppArmor profile,MySQL 有

装 nginx 和 mysql-server 之后:

# ls /etc/apparmor.d/ | grep -iE 'nginx|mysql'
usr.sbin.mysqld

nginx 在 Ubuntu 24.04 上没有 profile,所以 Ubuntu 上 nginx 的端口、目录问题基本和 AppArmor 无关;MySQL 则有。

8.2 MySQL 数据目录挪到 /data/mysql,起不来

# systemctl stop mysql; cp -a /var/lib/mysql /data/mysql
# (把 /etc/mysql/mysql.conf.d/mysqld.cnf 里的 datadir 改成 /data/mysql)
# grep -E '^\s*datadir' /etc/mysql/mysql.conf.d/mysqld.cnf
datadir = /data/mysql
# systemctl start mysql
Job for mysql.service failed because the control process exited with error code.

MySQL 自己的日志只说失败了:

# journalctl -u mysql --no-pager | tail -3          (截取)
Sep 21 16:05:14 ubuntu2404 systemd[1]: mysql.service: Main process exited, code=exited, status=1/FAILURE
Sep 21 16:05:14 ubuntu2404 systemd[1]: mysql.service: Failed with result 'exit-code'.
Sep 21 16:05:14 ubuntu2404 systemd[1]: Failed to start mysql.service - MySQL Community Server.

原因在内核日志里:

# journalctl -k --no-pager | grep -i 'apparmor="DENIED"' | tail -1          (截取)
Sep 21 16:05:14 ubuntu2404 kernel: audit: type=1400 audit(1790006714.780:121): apparmor="DENIED" operation="open" class="file" profile="/usr/sbin/mysqld" name="/data/mysql/binlog.index" pid=4205 comm="mysqld" requested_mask="wrc" denied_mask="wrc" fsuid=104 ouid=104

修法:把新路径加进 profile 的本地覆盖文件(别改主文件,升级时会被覆盖),然后重载:

# cat >> /etc/apparmor.d/local/usr.sbin.mysqld <<'AA'
/data/mysql/ r,
/data/mysql/** rwk,
AA
# apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld; echo "parser 退出码=$?"
parser 退出码=0
# systemctl start mysql; systemctl is-active mysql; mysql -NBe 'select @@datadir'
active
/data/mysql/

排查时想确认是不是 AppArmor,可以临时切到 complain 模式(只记录不拦截),确认完切回来:

# aa-complain /etc/apparmor.d/usr.sbin.mysqld
Setting /etc/apparmor.d/usr.sbin.mysqld to complain mode.
# aa-enforce /etc/apparmor.d/usr.sbin.mysqld
Setting /etc/apparmor.d/usr.sbin.mysqld to enforce mode.


  1. 查端口的工具:三台都没有 netstat ✅

CentOS 7Rocky 9Ubuntu 24.04
`ss`✅✅✅
`netstat`❌ `command not found`❌❌
`lsof`❌❌❌
`fuser`❌✅✅

连 CentOS 7 这台都没装 net-tools。与其每台去装 netstat,不如直接用 ss -lntp(三台都有):

# ss -lntp | head -4          (CentOS 7)
State      Recv-Q Send-Q Local Address:Port               Peer Address:Port              
LISTEN     0      128          *:22                       *:*                   users:(("sshd",pid=1167,fd=3))
LISTEN     0      100    127.0.0.1:25                       *:*                   users:(("master",pid=1434,fd=13))
LISTEN     0      128       [::]:22                    [::]:*                   users:(("sshd",pid=1167,fd=4))

另外 nc -zv -w 3 <IP> <端口> 是从外面测端口最顺手的一条(带超时,别干等);nmap -Pn -p <端口> <IP> 能给出 open / closed / filtered 三种状态。


  1. 本篇速查

排查顺序:

1. 本机 ss -lntp        → 服务起来没?监听的是 0.0.0.0 还是 127.0.0.1?
2. 从外面 nc -zv        → refused / No route to host / 超时,对照 §1 的表
3. 防火墙               → firewall-cmd --list-all / ufw status numbered
4. SELinux / AppArmor   → getenforce + ausearch --input-logs;aa-status + journalctl -k | grep DENIED

想做的事firewalldufw
看状态`firewall-cmd --state`(退出码 252 = 没运行)`ufw status verbose`
开 8080/tcp`--add-port=8080/tcp --permanent` + `--reload``ufw allow 8080/tcp`
端口段`8083-8085/tcp`(减号)`8081:8083/tcp`(冒号)
限来源`--add-rich-rule='rule family="ipv4" source address="…" port protocol="tcp" port="…" accept'``ufw allow from … to any port … proto tcp`
删规则`--remove-port=… --permanent` + `--reload``ufw delete allow 8080/tcp`(v4/v6 一起删)
挡住时客户端看到`No route to host`超时(`limit` 触发时是 refused)

这一篇最容易踩的坑:

  1. 🔴 firewalld 挡端口回的是 No route to host,不是超时;而且挡住时看不出后面有没有服务。
  2. 🔴 --permanent 不 --reload 不生效;不加 --permanent 一次 reload 就没。
  3. 🔴 firewalld reload 冲掉手工 iptables 规则:CentOS 7 会,Rocky 9 不会(但会变成 firewalld 看不见的第二套规则)。
  4. 🔴 Rocky 9 上 iptables -L 是空的,规则在 nft list ruleset 里。
  5. 🔴 iptables -A 排在 REJECT 后面永远不命中,看 -v 的 pkts 计数。
  6. 🔴 ufw 忘了放行 22 就 enable:新连接进不来,旧会话还活着 —— 别关它。
  7. 🔴 ufw 按编号删只删 v4,v6 那条还在。
  8. 🔴 SELinux:bind() … Permission denied、反代 502 connect() … Permission denied,日志都不提 SELinux。
  9. 🔴 setsebool 不加 -P 重启失效;chcon 一次 restorecon 就没。
  10. ⚠️ 脚本里的 ausearch 要加 < /dev/null 或 --input-logs,否则会挂住。
  11. ⚠️ CentOS 7 上 semanage port -a 撞已定义端口会报错,要 -m;Rocky 9 自动改成修改。

下一篇

(三)服务起不来:systemctl、开机自启、定时任务,同样三台机器实测。