SELinux 故障排查与策略定制实战

很多运维遇到 SELinux 的第一反应是关掉它。关掉确实能解决问题,但也把系统的一道防线拆了。更专业的做法是:先定位是不是 SELinux 在拦,再针对性放行。本文讲一套标准流程:permissive 定位、audit 日志解读、audit2allow 定制策略与布尔值调整,全程不用关 SELinux。

先说定位方法:服务异常时先看 getenforce 与 ausearch,不要凭感觉关。80% 的“权限明明对、服务就是起不来”都是 SELinux 上下文或布尔值的问题,修上下文就能解决。

一、确认是不是 SELinux 在拦:permissive 定位法

定位分两步:先看当前模式,再临时切到 permissive 观察。permissive 模式下 SELinux 只记录、不拦截,服务恢复正常就说明是它在拦;服务还是起不来,问题在别处。定位完记得切回 enforcing。

<pre class="wp-block-syntaxhighlighter-code"># 当前模式与策略
getenforce
sestatus
# 临时切到 permissive(重启后失效,仅用于定位)
setenforce 0
systemctl restart <service>
# 服务恢复正常 → 确认是 SELinux 在拦;服务仍异常 → 问题在别处
# 定位完成后切回 enforcing
setenforce 1
getenforce</pre>

不要把 setenforce 0 当成修复:它是定位手段,不是解决方案。永久关闭要改 /etc/selinux/config,那是最后的手段,不是第一步。

二、读 audit 日志:找到被拦住的那条规则

SELinux 的拦截记录都在 audit 日志里,关键字是 avc: denied。用 ausearch 按时间与服务过滤,重点看 scontext(源上下文)、tcontext(目标上下文)与 tclass(对象类型),这三项决定了该放行什么。

# 查最近一小时的 SELinux 拒绝记录
ausearch -m avc -ts recent
# 按服务过滤(以 httpd 为例)
ausearch -m avc -ts recent -c httpd
# 用 sealert 给出可读的解释与建议(需安装 setroubleshoot-server)
sealert -a /var/log/audit/audit.log | head -60
# 看某个文件的上下文是否正确
ls -Z /var/www/html/index.html
ls -Zd /data/app

最常见的拒绝是文件上下文不对:Nginx/Apache 只能读 httpd_sys_content_t 的文件,自定义目录的文件默认是 default_t,会被直接拦住。修上下文比改策略更优先。

三、修文件上下文:semanage fcontext 的正确用法

文件上下文分两种改法:chcon 是临时改,restorecon 或系统 relabel 后会丢;semanage fcontext 是持久改,写进策略数据库,重启与 relabel 都保留。生产上一律用 semanage。

# 给自定义 Web 目录打上 httpd 可读的上下文(持久)
semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
restorecon -Rv /data/www
ls -Z /data/www/index.html
# 给自定义端口打上服务可用的端口类型(以 Nginx 监听 8080 为例)
semanage port -a -t http_port_t -p tcp 8080
semanage port -l | grep http_port_t
# 验证:服务重启后应正常,不再出现 avc denied
systemctl restart nginx
ausearch -m avc -ts recent -c nginx

四、布尔值:开关式的快速放行

很多常见场景 SELinux 提供了布尔值开关,不用写策略就能放行:httpd 连数据库、httpd 访问 NFS、允许用户家目录写文件等。先查有没有现成的布尔值,再考虑写自定义策略。

# 查与 httpd 相关的布尔值
getsebool -a | grep httpd
# 允许 httpd 连接数据库(持久生效,加 -P)
setsebool -P httpd_can_network_connect_db on
# 允许 httpd 访问 NFS 上的内容
setsebool -P httpd_use_nfs on
# 查看当前布尔值状态
getsebool httpd_can_network_connect_db
semanage boolean -l | grep httpd_can_network_connect_db

布尔值改动要加 -P 才是持久的,不加只在当前运行期生效,重启后丢。改之前先看布尔值的说明,确认它放行的范围是不是你想要的,不要图省事全开。

五、定制策略:audit2allow 的用法与边界

上下文与布尔值都解决不了时,才用 audit2allow 从 audit 日志生成自定义策略模块。流程:收集拒绝记录 → 生成策略 → 编译安装 → 验证。注意:audit2allow 生成的是“允许日志里出现过的所有操作”,要先确认这些操作本身是合法的,不要把攻击行为也放行了。

# 从最近的拒绝记录生成策略模块(先人工检查生成的规则)
ausearch -m avc -ts recent | audit2allow -M myapp
cat myapp.te
# 确认规则合理后再安装
semodule -i myapp.pp
# 验证:重启服务,确认不再出现新的 avc denied
systemctl restart myapp
ausearch -m avc -ts recent -c myapp
# 列出已安装的自定义模块
semodule -l | grep myapp

六、端口放行:服务换了端口,先改 SELinux 再查防火墙

服务换到非标准端口后,SELinux 会先于防火墙把它拦住:防火墙放行了、服务却还是起不来,日志里出现 AVC denied,就是端口上下文没放行。semanage port 可以把新端口加到对应服务的端口类型里,一次设置永久生效。排查端口问题时,先看 AVC 日志,再看防火墙,顺序反了会绕很多弯路。

# 看某个端口当前属于哪种 SELinux 类型
semanage port -l | grep -E 'http_port_t|ssh_port_t'
# 给 Nginx 加一个自定义监听端口(示例 8080)
semanage port -a -t http_port_t -p tcp 8080
# 验证端口已放行
semanage port -l | grep http_port_t
# 端口不再用时删掉,避免放行范围越放越大
semanage port -d -t http_port_t -p tcp 8080
# 定位:服务起不来先看 AVC 日志,确认是不是端口被拦
ausearch -m avc -ts recent | grep -i denied | tail -5
# 常见组合:Nginx 反代后端端口也要放行(httpd_can_network_connect 布尔值)
setsebool -P httpd_can_network_connect 1

端口放行最常见的错误是类型选错:Nginx 的端口要加到 http_port_t,而不是随便找一个类型。改完端口要重启服务验证,不要只看 semanage 的输出。端口清单要定期审计,历史遗留的放行端口及时清理,缩小暴露面。

七、容器与 SELinux:容器里的拒绝和宿主机不是一回事

容器里跑应用时,SELinux 会在宿主机层面再加一道隔离:容器进程的域受限,访问宿主机挂载卷时还受卷的上下文约束。最常见的现象是容器里明明有权限、挂载的卷却读不了,宿主机上看卷的上下文是 default_t,容器需要的是 container_file_t。排障时要分清是容器内的权限问题,还是宿主机 SELinux 的隔离在起作用。

# 看容器挂载卷的上下文(宿主机上执行)
ls -Zd /data/appdata
# 给挂载卷打上容器可用的上下文(-r 递归,目录用这个)
semanage fcontext -a -t container_file_t "/data/appdata(/.*)?"
restorecon -Rv /data/appdata
# 临时验证:用 :Z(私有卷)或 :z(共享卷)让运行时自动打标签
# docker run -v /data/appdata:/data:Z myapp:latest
# podman run -v /data/appdata:/data:Z myapp:latest
# 看容器进程的 SELinux 域
ps -eZ | grep container_t | head -5
# 容器日志里出现 permission denied 时,先查宿主机 AVC 日志
ausearch -m avc -ts recent | grep container_t | tail -10

容器场景下不要图省事直接关 SELinux 或给容器加 --privileged:前者丢了整台宿主机的隔离,后者等于把宿主机权限交给了容器。正确做法是给卷打对上下文、让容器跑在受限域里。Kubernetes 里可以用 seLinuxOptions 指定卷的上下文级别,Pod 级别的设置要和存储卷的上下文一起核对。

八、策略管理与版本控制:自定义策略也要进版本库

audit2allow 生成的策略模块如果只存在某台机器上,机器重装、扩容新节点时策略就丢了,问题会重新出现。自定义策略要像代码一样管理:模块文件进版本库、变更走评审、部署走配置管理,节点上线时自动安装。这样策略的变更有记录、可回滚,出问题也能快速定位是哪次变更引入的。

# 导出自定义策略模块(备份与版本管理用)
semodule -l | grep -v '^$' > /tmp/selinux-modules.txt
# 导出指定模块的策略包
# semodule -E myapp  # 导出为 .pp 文件,按需使用
# 从版本库恢复并安装策略模块
semodule -i /opt/selinux-policies/myapp.pp
semodule -l | grep myapp
# 验证策略生效:跑一次之前被拦的操作,看 AVC 日志是否干净
ausearch -m avc -ts recent | grep myapp | tail -5
# 回滚:删掉有问题的模块,服务恢复到之前的状态
semodule -r myapp
# 新节点上线时,用配置管理自动安装策略(Ansible 示例思路)
# - name: Install custom SELinux policy
#   command: semodule -i /opt/selinux-policies/myapp.pp
#   args:
#     creates: /etc/selinux/targeted/active/modules/400/myapp

策略版本化要做到三点:模块文件与生成它的 audit 日志一起存、每次变更写清原因与对应的服务、回滚步骤提前写好。系统大版本升级后,旧策略模块可能不兼容,升级前要在测试机上重新生成并验证,不要直接搬旧模块上生产。

九、避坑清单

  • 不要一出问题就关 SELinux:先 permissive 定位,再针对性放行。
  • 文件上下文用 semanage fcontext 持久改,不要只用 chcon 临时改。
  • 布尔值改动要加 -P 持久化,重启后才不会丢。
  • audit2allow 生成的策略要人工检查,不要直接安装未经审查的规则。
  • 新服务上线前,先在测试机上跑一轮 permissive,收集并固化所需策略,再上生产。
  • 系统大版本升级后,SELinux 策略可能变化,升级后要重新验证关键服务的上下文与布尔值。

发表回复

后才能评论