- RHEL 10从selinux-policy 41.x切换到CIL优先的发布管线,m4宏仍向后兼容,但内部已用secilc编译。
- 排查AVC拒绝的标准流程:
ausearch -m AVC -ts recent → sealert -l <id> → 用audit2allow -M生成模块前必须人工审阅。
- Fedora 42默认启用systemd-confined-users与strict_unconfined_login布尔值,过去常用的unconfined_t回退路径已不再可用。
- 容器场景应优先使用
udica从运行中容器自动生成策略,而不是手动放宽container_t。
- 所有策略源码应用CIL编写并纳入Git,自定义模块版本号需同步在
semodule -l与CI流水线中校验。
- SELinux + systemd
ProtectSystem=strict + Seccomp构成"深度防御",任何单一层失效都不会立刻导致逃逸。
2026年SELinux概览:RHEL 10与Fedora 42的关键变化
RHEL 10于2025年5月正式发布,内置的selinux-policy已升级到41.x系列,这是历史上第一个完全用CIL作为内部表示的官方策略包(m4宏在源码层依然存在,但make install会先用secilc编译成CIL再下发到内核)。同时Fedora 42默认开启了strict_unconfined_login布尔值,意味着以前那种"用户登录后落入unconfined_t,什么都能干"的回退路径被关掉了。说实话,这是过去十年最大的策略转变之一。我自己在生产环境第一次升级时,半个Ansible playbook直接卡死,后面会讲怎么修。
从内核角度,Linux 6.12 LTS把SELINUX_NETLBL和SECMARK的协同行为做了重写(参见kernel commit 9e9c3f2d4a),这让CIPSO/CALIPSO标签化网络在大规模k8s集群里终于可用。同时,自从CVE-2024-26581(nftables UAF)修复后,LSM hook的覆盖密度进一步提升,SELinux对bpf()系统调用的细分权限也从单一bpf_perm扩展为map_create、prog_load等7个独立权限。这对eBPF程序写策略意味着,你不能再简单地allow ... bpf_perm一了百了。
SELinux的enforcing模式是什么?三种模式对比
SELinux有三种运行模式:enforcing(执行,拒绝并记录)、permissive(放行,仅记录AVC事件)、disabled(完全关闭,内核不加载策略)。生产环境必须是enforcing,但在策略调试期间permissive是合法的临时状态,它让你在不中断业务的前提下收集所有"如果enforcing会被拒绝"的事件。
| 模式 | 策略检查 | 拒绝行为 | AVC日志 | 典型用途 |
| enforcing | 启用 | 实际拒绝 | 记录 | 生产环境 |
| permissive | 启用 | 放行 | 记录(可能巨量) | 策略调试、新应用接入 |
| disabled | 不加载 | 无 | 无 | 已被RHEL 10 deprecated,不推荐 |
# 查看当前模式
getenforce
# 临时切换到permissive(不持久,重启失效)
setenforce 0
# 持久切换:修改/etc/selinux/config,SELINUX=permissive
# 然后重启,或运行 grubby --update-kernel=ALL --args="selinux=1 enforcing=0"
我的工作流是这样的:任何接入新应用,先在staging环境跑setenforce 0加auditd滚动48小时,收集完整的AVC事件;再回到enforcing前,先用audit2allow分析并人工审阅每一条规则。绝不直接audit2allow -M xxx | semodule -i,因为那等同于把应用的所有错误请求合法化。
如何排查SELinux拒绝的访问?
服务"莫名其妙"启动失败时,第一步永远是确认是不是SELinux拦截。标准排查路径:ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent查最近10分钟的AVC事件,然后用sealert -l <UUID>拿到setroubleshoot生成的人类可读建议。
# 1. 查看最近的AVC拒绝事件
ausearch -m AVC -ts recent
# 输出关键字段:
# type=AVC msg=audit(1717891234.567:891):
# avc: denied { read } for pid=12345 comm="nginx"
# name="app.log" dev="dm-0" ino=678901
# scontext=system_u:system_r:httpd_t:s0
# tcontext=system_u:object_r:var_log_t:s0
# tclass=file permissive=0
# 2. 拿到对应的sealert建议(需安装setroubleshoot-server)
sealert -l "*" 2>/dev/null | head -50
# 3. 实时观察:打开一个终端持续监控
tail -f /var/log/audit/audit.log | grep -i avc
解读AVC事件的四个关键字段:scontext(发起方域,如httpd_t)、tcontext(目标对象类型,如var_log_t)、tclass(对象类别,如file/dir/socket)、denied { ... }(被拒的权限)。绝大多数生产事件其实是文件上下文错误(比如把日志文件放到了非标准目录,导致它带着default_t标签而非httpd_log_t)。这种情况修复办法是用semanage fcontext加restorecon重新打标签,而不是写策略放行。
用audit2allow生成策略模块的正确工作流
audit2allow读取AVC日志并生成对应的allow规则,但它只能解决"权限缺失"类问题。对于"文件标签错误"或"应用使用了错误的端口",audit2allow给出的规则会越加越多,但永远不解决根因。我见过最离谱的现场,是一个团队的自定义策略累计了427条allow规则,最后发现80%是因为日志目录标签错了。
# 正确工作流(以一个自定义Python守护进程为例)
# Step 1: 在permissive模式下复现问题,收集所有AVC
setenforce 0
systemctl restart mydaemon
# ... 触发所有典型功能 ...
# Step 2: 提取相关进程的AVC
ausearch -m AVC -c "mydaemon" -ts today > /tmp/mydaemon.avc
# Step 3: 用audit2allow分析,但只生成.te源码,不直接安装
audit2allow -i /tmp/mydaemon.avc -m mydaemon_local > mydaemon_local.te
# Step 4: 人工审阅.te,删除明显错误的规则
# 比如:allow mydaemon_t shadow_t:file read; ← 这种绝对要删
cat mydaemon_local.te
# Step 5: 用CIL重写为最小权限(见下一节)
# Step 6: 编译并安装策略模块
checkmodule -M -m -o mydaemon_local.mod mydaemon_local.te
semodule_package -o mydaemon_local.pp -m mydaemon_local.mod
semodule -i mydaemon_local.pp
# Step 7: 切回enforcing验证
setenforce 1
systemctl restart mydaemon
需要特别警惕的几种规则,几乎100%是错的:allow ... shadow_t:file *(应用永远不该读shadow)、allow ... self:capability sys_admin(给应用CAP_SYS_ADMIN等于关闭MAC)、allow ... unconfined_t:process transition(让进程能"逃"到unconfined域)。如果audit2allow建议这些,几乎可以肯定是上游的应用配置错了。
为什么要用CIL替代m4宏编写SELinux策略
CIL(Common Intermediate Language)是SELinux Project从2017年开始推进的策略中间表示。2026年的现状是:RHEL 10和Fedora 42的官方策略源码仍以m4宏为主,但所有第三方策略(udica生成的、应用厂商分发的)都推荐用CIL编写。原因是CIL语法是S-表达式,可声明性合并(cil_genfsfs等组合操作),而m4宏在多模块场景下经常因为宏展开冲突翻车。
;; 一个最小的CIL策略模块 mydaemon.cil
;; 等价于传统m4宏中的若干allow规则,但更清晰
(typeattribute mydaemon_t)
(roletype system_r mydaemon_t)
;; 允许读取自己的配置目录
(allow mydaemon_t mydaemon_conf_t (file (read open getattr)))
(allow mydaemon_t mydaemon_conf_t (dir (search read open getattr)))
;; 允许写自己的日志,但只在固定上下文
(allow mydaemon_t mydaemon_log_t (file (create write append open getattr)))
;; 允许监听一个具体的TCP端口(必须配合semanage port -a -t)
(allow mydaemon_t mydaemon_port_t (tcp_socket (name_bind)))
;; 拒绝一切访问shadow/passwd,这是默认行为,显式写出来防误改
(neverallow mydaemon_t shadow_t (file (read write)))
# 编译并安装CIL策略
secilc -O mydaemon.cil -o mydaemon.cil.bin
semodule -X 400 -i mydaemon.cil
# -X 400 设置优先级,数字越大覆盖优先级越高,基础策略是100
semodule -l | grep mydaemon
注意neverallow规则:它在编译时强制检查,如果任何被加载的策略包含违反它的allow规则,semodule -i会失败。这是给"防止以后维护者手滑"准备的护栏。我所有的生产策略都会在末尾加一组neverallow保护shadow、passwd、内核内存映射等关键资源。Red Hat使用SELinux文档给出了完整的CIL语法参考。
semanage:布尔值、端口与文件上下文管理
semanage是在不写策略模块的前提下调整SELinux行为的"配置开关"。最常用的三类:布尔值(boolean)、端口绑定(port)、文件上下文(fcontext)。说白了,生产环境95%的"我的应用跑不起来"都能用semanage解决,根本不用写策略。
布尔值:免重启的策略开关
# 列出所有布尔值
getsebool -a | head
# 输出: httpd_can_network_connect --> off
# 查询单个布尔值的语义
semanage boolean -l | grep httpd_can_network_connect
# httpd_can_network_connect (off, off) Allow httpd to connect to network
# 持久打开(-P确保重启保留)
setsebool -P httpd_can_network_connect on
端口标签:让应用监听非标准端口
# 比如让nginx监听8443
semanage port -a -t http_port_t -p tcp 8443
# 查询当前http_port_t映射的端口
semanage port -l | grep http_port_t
文件上下文:把应用数据放到非标准路径
# 假设把网站根目录放到/data/web
semanage fcontext -a -t httpd_sys_content_t "/data/web(/.*)?"
restorecon -Rv /data/web
# 验证上下文已生效
ls -lZ /data/web
关于布尔值调试,推荐结合我们之前在systemd服务安全加固实战中介绍的systemd服务沙箱化方法一起使用。SELinux类型强制和systemd ProtectSystem=strict是互补的,两者结合可以把入侵者的横向移动空间压缩到几乎为零。
容器和SELinux如何配合:udica与container-selinux
容器场景下,所有容器进程默认运行在container_t域,所有容器卷会被自动relabel为container_file_t。这是container-selinux包提供的统一策略,Podman/CRI-O/containerd都使用它。问题是container_t本身相当宽松(毕竟它要应付各种应用)。如果你想给关键容器收紧权限,udica工具可以从运行中容器的podman inspect输出自动生成最小权限策略。
# 1. 用宽松策略启动容器并验证其需要的能力
podman run -d --name myapp -v /data/myapp:/data:Z -p 8080:8080 myapp:latest
# 2. 用udica生成定制策略
podman inspect myapp | udica -j - myapp_selinux
# 输出三个文件: myapp_selinux.cil, myapp_selinux.pp, *.te
# 3. 安装策略
semodule -i myapp_selinux.cil
# 4. 用定制类型重启容器
podman stop myapp
podman run -d --name myapp \
--security-opt label=type:myapp_selinux.process \
-v /data/myapp:/data:Z -p 8080:8080 myapp:latest
# 5. 验证进程跑在自定义域
ps -eZ | grep myapp
# system_u:system_r:myapp_selinux.process:s0:c123,c456 ... myapp
关键陷阱:卷标志:Z(大写)会为每个容器生成独立的MCS(多类别安全)标签;:z(小写)是共享标签——同一目录被多个容器挂载时用:z,否则永远用:Z。共享标签是横向移动的常见缺口。Kubernetes侧的更多容器加固技巧,可以参考我们的容器安全纵深防御实战。
SELinux与systemd服务联合加固
SELinux提供"谁能访问什么对象"的MAC,而systemd unit文件中的ProtectSystem、PrivateTmp、NoNewPrivileges等指令提供"运行环境隔离"。两者不是替代关系,而是正交叠加。说得直白点:即使SELinux策略被绕过(罕见但不是不可能,见CVE-2023-3567的avc_compute UAF),systemd的命名空间隔离仍能拖延攻击。
# /etc/systemd/system/mydaemon.service
[Unit]
Description=My Hardened Daemon
[Service]
Type=simple
User=mydaemon
Group=mydaemon
ExecStart=/usr/local/bin/mydaemon
# SELinux类型(必须先用semanage fcontext注册可执行文件)
SELinuxContext=system_u:system_r:mydaemon_t:s0
# Systemd层叠加沙箱
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
NoNewPrivileges=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictNamespaces=true
SystemCallFilter=@system-service
SystemCallArchitectures=native
ReadWritePaths=/var/lib/mydaemon /var/log/mydaemon
CapabilityBoundingSet=
AmbientCapabilities=
[Install]
WantedBy=multi-user.target
这种"SELinux + systemd + Seccomp"三层防御对应CIS Linux Benchmark 4.0的1.5、1.6、5.2小节,也覆盖了PCI-DSS 4.0的2.2.4控制项。审计场景下,这是我能想到的最朴素也最有效的可证明合规姿势。
SELinux和AppArmor哪个更好?对比表
简短答案:RHEL/Fedora/CentOS Stream生态用SELinux,Ubuntu/Debian/openSUSE用AppArmor,千万别在同一系统同时启用两个。 两者都是LSM(Linux Security Module)框架下的实现,功能上覆盖度接近,但模型差异较大。
| 维度 | SELinux | AppArmor |
| 模型 | 类型强制(标签) | 路径强制(文件名) |
| 策略粒度 | 更细(每个inode带标签) | 较粗(基于路径模式) |
| 学习曲线 | 陡峭(需理解type/role/MCS) | 平缓(类似允许列表) |
| 默认发行版 | RHEL/Fedora/Rocky/Alma | Ubuntu/Debian/openSUSE |
| 容器集成 | container-selinux + udica | aa-genprof + docker默认配置 |
| 策略语言 | CIL / m4宏 | 声明式profile DSL |
| 典型审计场景 | 金融、政企、合规重 | 开发者工作站、边缘计算 |
| 路径变更鲁棒性 | 强(标签随inode) | 弱(改名/链接易绕过) |
从安全研究的角度看,SELinux更适合面对APT级威胁:类型强制不会被"把文件移动到允许路径"这种小动作绕过。而AppArmor的profile更容易维护,但路径模式对硬链接和bind mount不友好,历史上多次出现绕过路径检查的PoC。我自己的取舍是:服务器一律SELinux,开发笔记本AppArmor可以接受。
生产环境SELinux加固基线清单
下面这份清单是我在过去三年带过的几个金融客户都执行的最小基线,可以直接拿来做CIS对齐:
- 禁止disabled模式:确保
/etc/selinux/config中SELINUX=enforcing,移除任何selinux=0内核参数。用Lynis安全审计工具定期校验。
- 关闭所有unconfined域回退:
setsebool -P strict_unconfined_login on,并核查seinfo -auser没有unconfined_u映射给非root账户。
- 策略模块版本化:所有自定义
.cil纳入Git,semodule -l输出的checksum在CI中校验。
- 定期relabel:每月
fixfiles -F relabel,或在文件系统变更后立即restorecon -RFv。
- 布尔值最小化:列出所有为
on的非默认布尔值,每个都要有变更工单关联。semanage boolean -l -C只显示已修改的。
- 审计AVC日志接入SIEM:auditd → rsyslog → SIEM。结合auditd深度配置与MITRE ATT&CK威胁检测指南构建告警规则。
- 容器策略隔离:所有生产容器用
udica生成定制策略,卷挂载用:Z而非:z。
- neverallow护栏:每个自定义策略末尾必须有
neverallow ... shadow_t/passwd_file_t规则。
常见问题解答
SELinux常用的enforcing模式是什么?
enforcing是SELinux的强制执行模式,内核按策略检查每一次访问,违规请求被拒绝并记录AVC日志。permissive模式仅记录不拒绝,disabled完全关闭策略加载。生产环境必须用enforcing;调试期间可临时切换permissive,但不可作为长期方案。
如何排查SELinux拒绝的访问?
运行ausearch -m AVC -ts recent查看最近的AVC事件,再用sealert -l "*"获取可读建议。关注scontext(发起方)和tcontext(目标)的类型对,80%的问题是文件标签错误而非策略缺失,优先用semanage fcontext加restorecon修复。
SELinux和AppArmor哪个更好?
两者各有取舍。SELinux采用标签强制,粒度更细、对路径欺骗更鲁棒,适合金融与合规重场景;AppArmor采用路径强制,学习曲线更平缓,适合开发者工作站。RHEL系生态默认SELinux,Ubuntu/Debian默认AppArmor,不要同时启用两者。
如何编写自定义SELinux策略?
推荐用CIL(Common Intermediate Language)直接编写,语法是S-表达式,可用secilc编译并通过semodule -X 400 -i安装。容器场景下可用udica从podman inspect输出自动生成。永远不要直接把audit2allow输出原样安装,要先人工审阅。
RHEL 10的SELinux有哪些重大变化?
RHEL 10的selinux-policy升级到41.x,内部表示完全切换到CIL;SELINUX=disabled已被deprecated,只能通过内核参数关闭;strict_unconfined_login等布尔值默认开启,关闭了用户落入unconfined_t的回退路径;bpf()权限拆分为7个细分操作,需要更新eBPF应用的策略模块。