ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SELinux实战:从进程域与文件类型解析强制访问控制原理

SELinux实战:从进程域与文件类型解析强制访问控制原理 1. 项目概述从一次真实的权限故障说起最近在排查一个线上服务的问题时遇到了一个典型的SELinux权限拒绝AVC denied日志。日志显示一个名为my_app的进程试图读取/data/custom/config.json文件但被拒绝了。初看之下文件权限是644进程用户也是my_app一切似乎都符合常规的Linux DAC自主访问控制规则但访问就是失败了。最终的罪魁祸首指向了SELinux更具体地说是进程的“域”domain和文件的“类型”type不匹配。这个案例让我意识到很多开发者包括曾经的我对SELinux的这两个核心概念——类型和域——的理解是模糊的甚至混为一谈。今天我就用一个完整的、可复现的案例把进程的域和文件的类型是如何协同工作来实施强制访问控制MAC的彻底讲透。简单来说你可以把SELinux想象成一个超级严格的保安系统。在普通的LinuxDAC世界里只要你是文件的所有者或者root你基本上可以对自己的文件为所欲为。但SELinuxMAC不同它制定了另一套规则即使你是root即使文件权限是777如果SELinux策略不允许你这个“角色”具体表现为进程的域去访问那个“资源”具体表现为文件的类型那么访问也会被断然拒绝。这里的“角色”对应的就是进程的域domain而“资源”的标签就是文件的类型type。理解它们如何配对是解决SELinux问题的关键。2. 核心概念拆解域Domain与类型Type的本质区别在深入案例之前我们必须先厘清这两个最易混淆的概念。它们在SELinux策略语言中虽然都使用type关键字声明但在运行时扮演的角色截然不同。2.1 域Domain进程的执行上下文域本质上是一个进程在运行时所处的安全上下文。它定义了该进程被允许执行哪些操作如读、写、执行、绑定端口等。你可以把域理解为进程的“身份证”或“工作证”上面写着这个进程被授权可以干什么。如何查看使用ps -Z命令。例如查看Web服务器进程ps -Z -C nginx。你会看到类似system_u:system_r:httpd_t:s0的输出其中第三部分httpd_t就是nginx进程的域。关键特性动态性进程的域可以转换。一个进程从启动到运行其域可能发生变化。例如用户登录shell初始可能是unconfined_t但执行特定二进制文件后可能切换到httpd_t域。继承与转换进程域的转换由策略规则严格定义。通常一个可执行文件具有特定的类型如httpd_exec_t被运行时会触发域转换使新进程进入预设的域如httpd_t。这是SELinux控制进程行为的关键机制。2.2 类型Type文件对象的安全标签类型是附加在文件、目录、端口、套接字等系统对象Object上的安全标签。它定义了该对象可以被哪些域的进程以何种方式访问。你可以把类型理解为资源的“分类标签”或“通行证级别”。如何查看使用ls -Z命令。例如查看Web目录ls -Z /var/www/html/。你会看到类似system_u:object_r:httpd_sys_content_t:s0的输出其中第三部分httpd_sys_content_t就是文件/目录的类型。关键特性静态性相对文件对象的类型通常在创建时由父目录的上下文或策略规则决定并在生命周期内相对稳定。当然管理员可以用chcon或restorecon命令修改它。分类控制通过给不同用途的文件打上不同的类型标签可以实现精细的访问控制。例如将网页内容标记为httpd_sys_content_t将脚本标记为httpd_sys_script_exec_t将上传目录标记为httpd_sys_rw_content_t然后针对每种类型设置不同的访问规则。2.3 一句话总结区别域Domain是贴在“动作执行者”进程身上的标签决定了它能做什么。类型Type是贴在“被操作对象”文件等资源身上的标签决定了它允许被谁、以何种方式操作。SELinux策略的核心就是定义了一系列规则来描述某个“域”对某个“类型”拥有何种“权限”允许、拒绝、审核。3. 实战案例构建一个受SELinux管控的自定义应用光说不练假把式。我们通过一个完整的案例亲手创建一个自定义应用并观察SELinux的域和类型如何介入其生命周期。我们的目标是创建一个守护进程my_daemon它需要读取/etc/my_app/config.conf的配置并向/var/log/my_app.log写入日志。3.1 环境准备与初始状态检查首先确保你的系统以RHEL/CentOS/Fedora为例SELinux处于强制模式Enforcing。# 检查SELinux状态 getenforce # 输出应为 Enforcing # 安装必要的策略开发工具如果尚未安装 # 对于RHEL/CentOS 7/8: sudo yum install policycoreutils policycoreutils-python setools setools-console setroubleshoot -y # 对于Fedora/RHEL 9: sudo dnf install policycoreutils policycoreutils-python-utils setools setools-console setroubleshoot -y创建我们的应用目录和文件sudo mkdir -p /etc/my_app /var/log/my_app /usr/local/bin sudo touch /etc/my_app/config.conf sudo touch /var/log/my_app.log # 创建一个简单的模拟守护进程脚本 sudo tee /usr/local/bin/my_daemon.sh EOF #!/bin/bash # 模拟读取配置 echo “尝试读取配置...” /dev/null cat /etc/my_app/config.conf 21 # 模拟写入日志 echo “$(date): Daemon is running.” /var/log/my_app.log sleep 60 EOF sudo chmod x /usr/local/bin/my_daemon.sh现在查看这些文件在SELinux下的默认标签ls -Z /usr/local/bin/my_daemon.sh /etc/my_app/config.conf /var/log/my_app.log你会看到类似这样的输出-rwxr-xr-x. root root unconfined_u:object_r:usr_t:s0 /usr/local/bin/my_daemon.sh -rw-r--r--. root root unconfined_u:object_r:etc_t:s0 /etc/my_app/config.conf -rw-r--r--. root root unconfined_u:object_r:var_log_t:s0 /var/log/my_app.log注意第三列的类型脚本是usr_t配置文件是etc_t日志文件是var_log_t。这些都是系统默认的类型。进程的域呢如果我们现在以root身份运行这个脚本由于是在不受限制的上下文中启动其域很可能是unconfined_t。3.2 首次运行与权限拒绝分析让我们以root身份在后台运行这个脚本并通过ps和审计日志观察。sudo /usr/local/bin/my_daemon.sh pid$! sudo ps -Z -p $pid # 观察进程域很可能是 unconfined_t sudo tail -f /var/log/audit/audit.log | grep -i avc # 在另一个终端查看实时审计日志此时脚本可能运行“正常”因为unconfined_t域权限极大几乎不受限制。但这不符合最小权限原则。我们希望为它创建一个专属的、受限的域。关键步骤触发真实的AVC拒绝为了看到SELinux的拦截效果我们需要先让进程在一个受限的域中运行。一个快速的方法是使用runcon命令临时指定一个域来运行进程。我们用一个已知的、权限较少的域如staff_t如果系统有的话来测试。# 首先确保 staff_t 域存在且可用于登录会话。更直接的方式是模拟一个策略未允许的场景。 # 我们直接开始编写策略在策略生效前进程会因为缺少规则而被拒绝。更好的方法是我们直接为my_daemon定义策略。当策略只定义了域但未允许其访问etc_t和var_log_t类型时拒绝就会发生。3.3 为自定义应用创建SELinux策略模块我们将创建一个最小化的SELinux策略模块定义my_daemon_t域并逐步添加允许规则。3.3.1 创建策略模块文件 (.te).te文件是类型强制Type Enforcement文件是策略的核心。sudo tee /tmp/my_daemon.te ‘EOF’ policy_module(my_daemon, 1.0) # 1. 声明新的进程域类型 type my_daemon_t; # 将其定义为进程域并关联到默认角色和用户 domain_type(my_daemon_t) # 可选声明一个用于域转换的入口点类型用于可执行文件 type my_daemon_exec_t; files_type(my_daemon_exec_t) # 标记为文件类型 # 2. 定义域转换当执行标有 my_daemon_exec_t 类型的文件时进程应切换到 my_daemon_t 域 # 这行规则是关键它连接了文件类型my_daemon_exec_t和进程域my_daemon_t allow my_daemon_t my_daemon_exec_t:file entrypoint; domain_auto_trans(unconfined_t, my_daemon_exec_t, my_daemon_t) # 含义如果处于 unconfined_t 域的进程执行了类型为 my_daemon_exec_t 的文件 # 则新进程自动auto_trans转换到 my_daemon_t 域。 # 3. 允许进程的一些基本权限 # 允许my_daemon_t域使用终端、信号等 unconfined_domain(my_daemon_t) # 这是一个宏会授予大量基本权限便于测试。生产环境应细化。 # 或者更精细地 # allow my_daemon_t self:process *; # allow my_daemon_t self:fd *; # allow my_daemon_t self:fifo_file rw_file_perms; # allow my_daemon_t self:unix_stream_socket *; # 4. 初始阶段我们故意**不**添加访问配置和日志文件的规则。 # 这样当进程尝试访问时就会被SELinux拒绝我们可以捕获到AVC日志。 # allow my_daemon_t etc_t:file read; # 先注释掉 # allow my_daemon_t var_log_t:file { append create }; # 先注释掉 EOF3.3.2 编译并安装策略模块# 切换到模块目录 cd /tmp # 编译 .te 文件为 .mod 文件 sudo checkmodule -M -m -o my_daemon.mod my_daemon.te # 将 .mod 文件打包为策略包 .pp 文件 sudo semodule_package -o my_daemon.pp -m my_daemon.mod # 加载策略模块到内核 sudo semodule -i my_daemon.pp3.3.3 应用类型标签并测试现在我们需要将可执行文件标记为我们定义的入口点类型my_daemon_exec_t这样执行它时才会触发域转换。sudo chcon -t my_daemon_exec_t /usr/local/bin/my_daemon.sh # 为了确保重启后标签不变最好也修改文件上下文规则可选后续做 # sudo semanage fcontext -a -t my_daemon_exec_t ‘/usr/local/bin/my_daemon.sh’ # sudo restorecon -v /usr/local/bin/my_daemon.sh现在再次运行脚本并检查进程域sudo /usr/local/bin/my_daemon.sh pid$! sleep 1 sudo ps -Z -p $pid你应该能看到进程的域变成了my_daemon_t这说明我们的域转换规则生效了。LABEL PID TTY STAT TIME COMMAND unconfined_u:unconfined_r:my_daemon_t:s0-s0:c0.c1023 12345 pts/0 S 0:00 /bin/bash /usr/local/bin/my_daemon.sh现在检查审计日志/var/log/audit/audit.log或者使用ausearch命令sudo ausearch -m avc -ts recent | grep my_daemon_t你大概率会看到类似下面的AVC拒绝信息deniedtypeAVC msgaudit(1714038400.123:456): avc: denied { read } for pid12345 comm“cat” scontextunconfined_u:unconfined_r:my_daemon_t:s0-s0:c0.c1023 tcontextunconfined_u:object_r:etc_t:s0 tclassfile permissive0 typeAVC msgaudit(1714038400.124:457): avc: denied { append } for pid12345 comm“my_daemon.sh” scontext...my_daemon_t... tcontext...var_log_t... tclassfile permissive0日志解读avc: denied: 表示SELinux拒绝了这次访问。{ read }/{ append }: 被拒绝的操作。pid12345 comm“cat”/“my_daemon.sh”: 发起操作的进程。scontext...my_daemon_t...:源上下文Source Context即进程的域。这是“动作执行者”的标签。tcontext...etc_t.../...var_log_t...:目标上下文Target Context即文件对象的类型。这是“被操作对象”的标签。tclassfile: 目标对象类别是文件。这就是进程域my_daemon_t与文件类型etc_t, var_log_t协同工作的现场证据策略中没有允许my_daemon_t域对etc_t类型的文件进行read操作也没有允许它对var_log_t类型的文件进行append操作因此访问被拒绝。3.4 完善策略添加允许规则现在我们根据AVC日志的提示修改策略文件添加允许规则。编辑/tmp/my_daemon.te文件取消注释并修改相关行sudo tee -a /tmp/my_daemon.te ‘EOF’ # 5. 根据AVC日志添加允许规则 # 允许 my_daemon_t 域读取 etc_t 类型的文件 allow my_daemon_t etc_t:file read; # 允许 my_daemon_t 域在 var_log_t 类型的文件上追加内容和获取属性 allow my_daemon_t var_log_t:file { append create getattr open write }; # 注意create 权限是当文件不存在时需要创建的。getattr和open通常是必需的。 # 6. 允许使用标准输出等 allow my_daemon_t devpts_t:chr_file write; EOF重新编译并更新模块cd /tmp sudo checkmodule -M -m -o my_daemon.mod my_daemon.te sudo semodule_package -o my_daemon.pp -m my_daemon.mod # 更新模块-i 会覆盖同名旧模块 sudo semodule -i my_daemon.pp现在杀死旧的进程重新运行脚本sudo pkill -f my_daemon.sh sudo /usr/local/bin/my_daemon.sh pid$! sleep 2 sudo tail -5 /var/log/my_app.log如果一切顺利你将看到日志被成功写入且审计日志中不再有相关的AVC拒绝信息。这证明我们添加的规则allow my_daemon_t etc_t:file read;已经生效进程域my_daemon_t现在被允许对文件类型etc_t和var_log_t执行特定操作。3.5 优化策略使用更精确的类型然而直接允许my_daemon_t访问通用的etc_t和var_log_t类型并不安全。这相当于给了这个进程读取所有/etc下文件和向所有/var/log下文件追加内容的能力如果其他文件也是var_log_t。更好的做法是定义专有的文件类型。3.5.1 定义专有文件类型修改/tmp/my_daemon.te为配置文件和日志文件定义专属类型sudo tee /tmp/my_daemon_v2.te ‘EOF’ policy_module(my_daemon, 1.1) # 版本号更新 type my_daemon_t; domain_type(my_daemon_t) type my_daemon_exec_t; files_type(my_daemon_exec_t) # 定义专有的配置文件类型和日志文件类型 type my_daemon_config_t; files_type(my_daemon_config_t) type my_daemon_log_t; logging_log_file(my_daemon_log_t) # 使用宏它包含了log文件相关的通用规则 # 域转换规则 allow my_daemon_t my_daemon_exec_t:file entrypoint; domain_auto_trans(unconfined_t, my_daemon_exec_t, my_daemon_t) # 基本权限 unconfined_domain(my_daemon_t) # 仍用宏简化实际应细化 # ---- 核心规则使用专有类型 ---- # 允许进程读取自己的配置文件类型 allow my_daemon_t my_daemon_config_t:file read; # 允许进程对自己的日志类型进行日志文件操作 allow my_daemon_t my_daemon_log_t:file { append create getattr open write }; # 同时需要允许日志轮转等工具如logrotate管理这些日志文件。 # 通常 logging_log_file 宏已经处理了与 logrotate 等工具的交互。 EOF3.5.2 应用新的文件上下文编译安装新模块后我们需要将实际的文件路径映射到新的类型。cd /tmp sudo checkmodule -M -m -o my_daemon_v2.mod my_daemon_v2.te sudo semodule_package -o my_daemon_v2.pp -m my_daemon_v2.mod sudo semodule -i my_daemon_v2.pp # 删除旧模块避免冲突 sudo semodule -r my_daemon现在使用semanage fcontext和restorecon来永久设置文件标签# 添加文件上下文规则 sudo semanage fcontext -a -t my_daemon_config_t ‘/etc/my_app(/.*)?’ sudo semanage fcontext -a -t my_daemon_log_t ‘/var/log/my_app(/.*)?’ sudo semanage fcontext -a -t my_daemon_exec_t ‘/usr/local/bin/my_daemon.sh’ # 应用规则恢复文件的安全上下文 sudo restorecon -Rv /etc/my_app /var/log/my_app /usr/local/bin/my_daemon.sh # 验证标签 ls -Z /etc/my_app/ /var/log/my_app.log /usr/local/bin/my_daemon.sh现在文件的类型已经变成了我们自定义的my_daemon_config_t和my_daemon_log_t。再次运行守护进程它应该能正常工作并且策略范围被严格限定在自有文件类型上安全性更高。4. 深度解析策略规则如何桥接域与类型通过上面的案例我们看到了allow规则是如何将进程域和文件类型联系起来的。让我们深入看看策略规则的语法和逻辑。一个基本的TEType Enforcement规则格式如下allow source_type target_type : target_class permission_set;source_type: 源类型通常是进程的域。target_type: 目标类型通常是文件、目录、端口、套接字等对象的类型。target_class: 目标类别如file,dir,tcp_socket,process等。permission_set: 权限集合如{ read write },{ create getattr },name_bind等。规则解读示例allow my_daemon_t my_daemon_config_t:file read;谁被允许域为my_daemon_t的进程。对什么操作对类别为file的对象。哪个/哪些对象类型为my_daemon_config_t的文件。允许什么权限read读权限。域转换规则是另一个关键桥梁domain_auto_trans(unconfined_t, my_daemon_exec_t, my_daemon_t)在什么条件下当一个域为unconfined_t的进程。执行了什么执行了一个类型为my_daemon_exec_t的文件。结果是什么新产生的进程的域自动转换为my_daemon_t。这条规则将文件的类型(my_daemon_exec_t) 与进程的域(my_daemon_t) 动态地关联起来是进程获得其身份域的起点。5. 高级主题与排查技巧5.1 使用工具分析AVC拒绝当遇到权限问题时audit2why和audit2allow是你的好朋友。# 1. 从审计日志中直接获取人类可读的解释 sudo ausearch -m avc -ts recent | audit2why # 2. 根据最近的AVC拒绝生成一个允许这些操作的策略模块.te文件 sudo ausearch -m avc -ts recent | audit2allow -m my_daemon # 这会输出建议的allow规则。**务必仔细审查**确保规则最小化、安全。 # 3. 直接生成并安装一个临时策略模块生产环境慎用 sudo ausearch -m avc -ts recent | audit2allow -M my_daemon_temp sudo semodule -i my_daemon_temp.pp注意audit2allow生成的规则可能过于宽泛。它给出的建议是“允许这次被拒绝的操作”但可能不是最精确的类型。最佳实践是参考其输出然后手动编写或修改策略使用更具体的类型如我们定义的专有类型而不是直接允许访问通用类型如etc_t。5.2 布尔值Booleans动态调整策略SELinux提供布尔值可以在不修改和重新编译策略模块的情况下动态开启或关闭一组规则。这对于适应不同的应用场景非常有用。# 查看与HTTPD相关的布尔值 getsebool -a | grep httpd # 允许HTTPD访问NFS文件 sudo setsebool -P httpd_use_nfs on-P选项使设置永久生效。布尔值实际上是策略中条件语句的开关。5.3 常见问题排查流程确认问题是否由SELinux引起将SELinux切换到许可模式setenforce 0看问题是否消失。如果消失基本确定是SELinux问题。查看审计日志tail -f /var/log/audit/audit.log或使用ausearch。AVC拒绝信息是首要线索。分析AVC日志使用audit2why理解拒绝原因。检查安全上下文使用ps -Z和ls -Z确认进程域和文件类型是否符合预期。制定解决方案临时解决使用audit2allow生成并安装临时模块仅用于测试。修正文件上下文如果文件标签不对使用chcon临时修改或使用semanage fcontext和restorecon永久修正。调整布尔值如果存在相关布尔值尝试调整。自定义策略对于自定义应用像我们案例中那样编写专用策略模块是最佳实践。恢复强制模式解决方案测试成功后务必切回强制模式setenforce 1。5.4 实操心得与避坑指南最小权限原则是金科玉律永远只授予进程完成其功能所必需的最小权限。从完全拒绝开始根据AVC日志逐一添加allow规则。善用专有类型不要让你的进程域去访问广泛的系统类型如etc_t,var_t。为你的应用数据定义专属的类型如myapp_config_t,myapp_data_t这能极大提升安全性。理解域转换确保你的可执行文件被打上了正确的入口点类型*_exec_t并且域转换规则正确。这是进程获得正确域的第一步。audit2allow是双刃剑它是一个强大的学习工具但不要盲目信任其生成的策略。一定要人工审查将其作为编写精确策略的参考而不是最终方案。持久化文件上下文使用chcon修改的标签在文件系统重标如restorecon或包更新后可能会丢失。始终通过semanage fcontext添加规则再用restorecon应用这样才能持久化。测试策略模块在将新模块加载到生产环境前先在测试环境或通过semodule -i后密切监控审计日志确保没有意料之外的拒绝或过度授权。回到开头的案例那个my_app无法读取/data/custom/config.json的问题最终的解决方案就是检查进程域和文件类型发现文件类型是default_t而非应用预期的类型。通过添加正确的文件上下文规则 (semanage fcontext -a -t myapp_config_t ‘/data/custom(/.*)?’) 并执行restorecon或者直接在策略中允许进程域访问default_t类型安全性较低问题得以解决。整个过程的核心始终围绕着识别“动作执行者”进程域和“被操作对象”文件类型并在SELinux策略中为它们建立正确的、最小化的访问桥梁。希望这个案例能帮你彻底分清SELinux的类型和域并在下次遇到权限问题时能自信地拿起这些工具进行排查。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进