SELinux 导致服务异常:实战排查与修复

很多 Linux 服务器上的“灵异”故障,根因其实不是代码也不是网络,而是 SELinux。典型表现是:服务进程明明在跑、端口也在监听,可一访问就 403 或连接被拒,关掉防火墙也没用。本文以 Nginx 反代、自定义 Web 目录 403、容器挂载为例,手把手用 SELinux 三板斧(getenforce / ausearch / semanage)定位并修复 AVC 拒绝,既保住系统安全又解决访问问题。

一、SELinux 到底拦了什么

SELinux(Security-Enhanced Linux)是内核级的强制访问控制(MAC)机制,它和传统的 DAC(文件 rwx 权限)是两套独立体系。一句话理解:即使文件权限是 755、进程以 root 运行,只要 SELinux 的类型(type)规则不允许,访问照样被拒——这就是所谓的“默认拒绝”。

在 SELinux 的 Targeted 策略里,每个进程运行在某个域(domain)中,每个文件/端口带一个类型(type)标签。规则只允许“域”访问特定“类型”。例如 Nginx 进程跑在 httpd_t 域,默认只能读 httpd_sys_content_t 类型的网页文件;如果你把站点放在 /srv/web 却没改标签,403 就来了。

为什么“关了防火墙还是不行”

这是新手最容易踩的坑:firewalld / iptables 管的是“网络能不能进来”,SELinux 管的是“进程能不能碰这个资源”。两者是叠加关系,关掉前者并不能绕过后者。排查访问类故障时,一定要把 SELinux 纳入怀疑名单,尤其是刚迁移目录、改了端口、启了容器之后。

二、三类最常被 SELinux 拦住的场景

下面结合真实排障经验,列出最高频的三类“服务在跑却用不了”的 SELinux 拦截。

1. 端口被拒:Nginx 反代连不上后端

把 Nginx 配成反向代理,后端监听在非标准端口(如 8080、9000)时,httpd_t 域默认只允许连接 http_port_t 里的端口。结果就是 Nginx 启动正常,但 proxy_pass 时报“Permission denied”。相关内容可参考本站 Nginx 反向代理实战。

2. 文件被拒:自定义 Web 目录返回 403

把网站根目录从 /var/www 挪到 /srv/web 或 /home 下,文件标签没跟着变,Nginx 读不到就 403。这不是权限问题,是类型不匹配。

3. 容器挂载目录被拒

Docker 把宿主机目录挂进容器后,容器内进程访问宿主机文件时也可能被 SELinux 拦,表现为“挂载成功却读不了”。这点常和 Docker 运维 的权限问题混淆。

这三类问题有一个共同信号:在日志和进程层面往往看不到明确的权限报错,但功能就是不通。养成一个肌肉记忆——只要遇到“明明该能访问却访问不了”的怪事,第一时间 getenforce 看一眼模式,常常能省下几个小时瞎猜防火墙、查网络、翻代码的弯路。

三、三板斧快速定位 AVC 拒绝

AVC(Access Vector Cache)拒绝会被记到审计日志里。先用下面两条确认当前状态:

# 查看当前模式:Enforcing / Permissive / Disabled
getenforce

# 查看详细状态(模式、策略、布尔开关统计)
sestatus

如果处于 Enforcing,而服务又异常,下一步直接挖 AVC 拒绝日志:

# 查看最近的 AVC 拒绝记录
ausearch -m AVC -ts recent

# 把拒绝记录转成可读的允许规则(仅用于分析,别直接管道执行!)
ausearch -m AVC -ts recent | audit2allow

# 实时跟踪:开两个终端,一边复现故障一边看拒绝
tail -f /var/log/audit/audit.log | grep AVC

看到形如 denied { read } for pid=... comm="nginx" name="index.html" dev="sda1" scontext=... tcontext=... tclass=file 的记录,基本就能锁定是哪个域、哪个目标类型被拦了。

四、临时与永久关闭(应急止血)

定位期间,可以先把 SELinux 切到 Permissive(宽容)模式:它会记录拒绝但不真正拦截,方便你确认“是不是 SELinux 的锅”。

# 运行时切换到 Permissive(立即生效,重启后失效)
setenforce 0

# 切回 Enforcing
setenforce 1

要永久生效,改配置文件(重启才生效):

# /etc/selinux/config
SELINUX=permissive      # enforcing | permissive | disabled
SELINUXTYPE=targeted

注意:disabled 和 permissive 不一样。disabled 是彻底关闭,重新开启需要重启并给全文件系统重新打标签(relabel),耗时且易出事;permissive 只是不拦截,标签体系还在。应急尽量用 permissive,不要直接 disabled。

五、推荐做法:最小权限放行(而不是关掉它)

SELinux 拦你是有道理的,正确姿势是按最小权限放行,而不是一怒之下关掉。下面三招覆盖 90% 场景。

5.1 放行自定义端口

# 查看 http 相关端口类型已允许的端口
semanage port -l | grep http_port_t

# 把 8080 加入 http_port_t,Nginx 反代即可连通
semanage port -a -t http_port_t -p tcp 8080

5.2 修复文件安全上下文

用 semanage fcontext 登记“目录→类型”的映射,再用 restorecon 应用,比临时 chcon 更持久(chcon 在 relabel 后会丢失)。

# 登记规则:/srv/web 及其子内容都标为 httpd_sys_content_t
semanage fcontext -a -t httpd_sys_content_t "/srv/web(/.*)?"

# 立即应用(递归、显示变更)
restorecon -Rv /srv/web

# 校验某个文件的当前类型
matchpathcon /srv/web/index.html

5.3 打开布尔开关

很多“服务能不能干某事”被抽象成布尔开关,改它最干净:

# 列出与 httpd 相关的布尔开关
getsebool -a | grep httpd

# 允许 httpd 发起网络连接(反向代理刚需)
setsebool -P httpd_can_network_connect on

# 允许 httpd 访问 NFS/CIFS 等共享文件
setsebool -P httpd_use_nfs on

这些开关与 Linux 服务器安全加固 是同一体系,加固时不该粗暴关 SELinux,而该精准放行。

一个容易踩的小坑:chcon 只是临时改标签,一旦文件系统重新打标签(relabel)或你手动执行了 restorecon,改动就会丢失,因此它只适合应急验证“是不是类型不对”。真正要持久生效,一律走 semanage fcontext 登记规则、再 restorecon 应用的组合,这样即使重打标签也不会回退。

六、容器场景:Docker 与 SELinux

Docker 默认会给挂载的卷打上 svirt_sandbox_file_t 等容器类型,但经常需要显式声明。最实用的两个挂载后缀:

# :z  让挂载目录在多个容器间共享(重新打标签)
docker run -v /data/app:/app:z myimage

# :Z  私有标签,仅当前容器可访问
docker run -v /data/app:/app:Z myimage

# 完全禁用对该容器的 SELinux 限制(不推荐,仅调试)
docker run --security-opt label=disable myimage

如果容器要给 cgroup 写数据,还需:setsebool -P container_manage_cgroup on。记住优先用 :z/:Z 而不是 label=disable,保留隔离收益。

七、排障清单:症状 → 命令 → 修复

症状快速诊断修复手段
Nginx 反代连接被拒ausearch -m AVC 见 comm=nginxsemanage port -a -t http_port_t -p tcp 端口
Web 目录 403matchpathcon 类型不符semanage fcontext + restorecon
进程读文件 deniedscontext/tcontext 不匹配chcon 临时或 fcontext 持久化
容器挂载读不了宿主机文件类型非容器型挂载加 :z / :Z
服务起不来也不报明显错setenforce 0 后恢复定位后最小权限放行,再 setenforce 1

八、小结

SELinux 看似“多管闲事”,实则是生产环境重要的纵深防御。排障口诀就一句:Permissive 定位、Enforcing 上线、别用 disabled 糊弄。先用 getenforce 确认模式,用 ausearch 抓 AVC 拒绝,再用 semanage + restorecon / setsebool 精准放行。把这套三板斧记牢,下次再遇到“服务在跑却用不了”的怪事,就能十分钟内定位收工。

上一篇 分布式 ID 生成实战:雪花算法与号段模式选型
下一篇 Java HttpClient 实战:用内置客户端调用 REST API
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]