第 3 章 SELinux 策略结构:策略文件类型、策略层次、模块化设计

聊到 SELinux 策略,很多新人第一反应就是「那一大堆 .te 文件到底是干啥的?」确实,策略文件乍一看挺吓人,但拆开来看其实就几类东西。我个人习惯把策略结构比作「乐高积木」—— 你有一堆基础砖块(类型、属性、规则),然后按层搭起来,最终组成完整的安全策略。

3.1 策略文件类型:三大基石

SELinux 策略由三种核心文件构成,每类文件承担不同角色。我用一个表格总结它们的区别:

文件类型 扩展名 核心作用
类型强制文件 .te 定义类型、属性、类型转换、关联规则
接口文件 .if 封装可复用的策略接口(类似函数库)
文件上下文文件 .fc 指定文件系统路径与类型的映射关系

3.2 .te 文件:策略的骨架

.te 文件是策略的主体。里面定义的东西包括:

  • 类型声明:type my_app_t;
  • 属性声明:attribute my_domain; ,然后给类型贴属性
  • allow 规则:allow my_app_t my_file_t:file read;
  • 类型转换:type_transition my_app_t exec_t:process my_app_t;

我在项目中遇到过有人把所有规则都写在一个超级大的 .te 文件里,结果维护起来简直噩梦。所以我建议你按模块拆,一个模块一个 .te 文件,比如 network.te、filesystem.te。这样后续做回归测试时,定位问题也会快很多。

避坑指南:千万别在 .te 文件里混写接口定义。我曾经接手过一个项目,.te 里写了 interface,导致其他模块引用时出现了诡异的依赖循环。接口请放到 .if 文件里。

3.3 .if 文件:策略的接口层

.if 文件有点像 C 语言的函数声明。它定义可被其他模块调用的策略接口。例如:

interface(`my_app_domtrans',`
    gen_require(`
        type my_app_t;
    ')
    domtrans_pattern($1, my_app_exec_t, my_app_t)
')

为什么要有 .if 文件?说白了就是为了复用。你想想看,假如多个模块都需要允许某个域转换,每个模块都写一遍相同的规则,不仅啰嗦,还容易出错。用接口封装后,其他模块只需一句:my_app_domtrans(unconfined_t) 就搞定了。

注意:.if 文件里的 interface 和 template 宏一定要写清楚参数说明。我见过一个团队因为接口文档缺失,同一段逻辑用了三种不同写法,调试时互相甩锅。嗯,血泪教训。

3.4 .fc 文件:文件类型绑定

.fc 文件用于将文件系统路径与安全上下文绑定。最常见的写法:

/opt/myapp(/.*)?    system_u:object_r:my_app_file_t:s0

这里要注意的是路径匹配的先后顺序。策略加载时,.fc 文件中的条目按顺序匹配,一旦匹配成功就停止。所以我个人习惯把更具体的路径放在前面,通配符放在后面,避免被粗匹配吃掉。

3.5 策略层次:从低层到高层的栈

SELinux 策略是分层构建的。从最底层开始:

  1. 基础策略(base):提供最核心的类型和权限,比如 kernel_t、unconfined_t,以及基本的安全类。
  2. 系统服务层(system):为 systemd、dbus、udev 等系统服务定义规则。
  3. 应用模块层(modules):为具体应用(如 Nginx、MySQL)定制的策略模块。
  4. 用户自定义层(custom policy):我们自己写的私有策略,就是绝大多数回归测试需要适配的地方。

为什么要分层?我举个例子你就懂了。假设基础策略升级了,如果你把所有规则都写在一个层级里,升级时可能会冲突;分成模块后,只需要重新编译有改动的模块,再加载就行了。回归测试也是同理,只测受影响的模块,能省下大量时间。

3.6 模块化设计:从单体到积木

SELinux 策略的模块化设计主要体现在几个方面:

  • 按功能拆分:每个守护进程或一组相关进程一个模块。
  • 显式依赖:模块通过 require 声明需要的外部类型/属性/接口。
  • 分层引用:低层模块为高层模块提供基础接口,高层模块不会反向依赖低层。

来看一个典型的模块目录结构:

policy/modules/
├── system/
│   ├── init.te
│   ├── init.if
│   └── init.fc
├── apps/
│   ├── nginx.te
│   ├── nginx.if
│   └── nginx.fc
└── custom/
    ├── myapp.te
    ├── myapp.if
    └── myapp.fc

我建议你把自己的回归用例策略也按这种模块方式组织。比如 regression_test_app1 一个模块,regression_test_app2 一个模块。这样每次覆盖时只需单独编译和加载,不用动整个策略源。

核心要点:模块化不只是一个设计理念,更是回归测试的基石。当你需要为某个用例新增权限时,你不会想去翻那个几千行的巨型策略文件的。

3.7 知识结构图:SELinux 策略的三层架构

SELinux 策略结构:文件类型 + 层次 + 模块 策略文件类型 .te 类型强制文件 .if 接口文件 .fc 文件上下文 策略层次 基础策略 (base) 系统服务层 (system) 应用模块层 (modules) 用户自定义层 (custom) 模块化设计:每个 .te / .if / .fc 组合为一个模块 • 模块独立编译:每个模块可以单独编译为 .pp 策略包 • 显式依赖声明:通过 require() 声明需要的外部类型/接口 • 层次间引用:低层模块为高层提供接口,无反向依赖

3.8 模块化带来的回归测试优势

讲真的,我刚入行时也不太明白为什么要分模块,觉得「麻烦,直接写在一起不就行了」。后来一次项目上线前,基础策略打了补丁,结果整个 .te 文件需要重新 review 一遍,那个酸爽……从那以后,我老老实实按模块拆分。

模块化对回归测试的好处很明显:

  • 隔离影响:修改一个模块的规则,其他模块不受影响。回归测试可以只验证该模块的用例。
  • 快速迭代:模块编译比全量编译快得多。举个例子,一个包含 50 个模块的策略,改一个模块的全量编译需要 3 分钟,而单独编译这个模块只要 10 秒。
  • 避免命名冲突:类型名在模块内是局部作用域,不同模块里定义同名类型也不会冲突。
我个人的一个小习惯:写回归测试用例策略时,尽量只引用接口,不要直接引用其他模块内部的类型。这样即使底层模块重构了数据类型,你的回归用例接口参数也不用改。怎么做到呢?就是多用 .if 里定义的宏,少写硬编码的 allow 规则。

3.9 实践建议:如何组织回归测试策略模块

最后分享一个我在团队里推广的目录结构:

regression_policy/
├── test_module_a/
│   ├── test_module_a.te
│   ├── test_module_a.if
│   └── test_module_a.fc
├── test_module_b/
│   ├── test_module_b.te
│   ├── test_module_b.if
│   └── test_module_b.fc
└── Makefile

每个测试模块对应一次回归测试场景。Makefile 里写好编译目标,比如 make test_module_a.pp。这样做的好处是,当某个场景回归失败时,你直接定位到对应的模块,不用大海捞针。

嗯,玩 SELinux 策略,模块化不仅仅是架构的追求,更是后期运维和回归测试的救命稻草。记住它。