SMACK安全模块实战:用标签实现嵌入式Linux强制访问控制

📅 发布时间:2026/10/21 18:51:17
SMACK安全模块实战:用标签实现嵌入式Linux强制访问控制
1. 从“根上”管权限为什么系统安全不能只靠一套机制前两年我一直在折腾嵌入式 Linux 系统的安全加固遇到过不少“权限明明配好了程序却还是能干坏事”的诡异场景。后来把内核安全模块这条线完整捋了一遍才算真正看明白问题出在哪。Linux 安全模块LSM不是单一某个功能而是一套插在内核里的通用框架SMACK 就是这框架里一个相当务实的安全模块实现。简单说SMACK 的全称是 Simplified Mandatory Access Control Kernel翻译过来就是“简化强制访问控制内核模块”。它最核心的价值是让系统里的每个进程、每个文件、每个网络连接都能挂上一个“安全标签”然后按照一套简单规则决定谁可以碰谁。比起传统的 Unix 权限owner/group/other 那一套SMACK 是真正在强制访问控制层面做约束即使 root 用户也没法随意绕过。因为这套检查发生在内核里任何用户态程序都躲不掉。这篇文章适合三类人看一类是做嵌入式或物联网设备安全方案、想把权限粒度做细的开发一类是在学习内核安全子系统想搞清楚 LSM 到底是咋工作的学生还有一类是用容器、跑多租户服务感觉现有隔离方式不够用、想找新思路的运维。我会把 SMACK 的定位、标签规则、实际操作流程、踩过的坑都摊开讲清楚带着你从零把一个启用了 SMACK 的小系统跑起来。当时我在某公司的嵌入式项目上需要让同一个系统同时跑多个业务模块模块之间数据要隔离但又不能像虚拟机那样重量级。SELinux 的策略规则太复杂团队学习成本高转了一圈选型最后敲定了 SMACK。它确实名副其实规则简单、标签直接、好上手但同时也有它自己的脾性和限制用好了是一把利器用不好也会踩进一些隐蔽的坑。2. 搞懂 LSM 框架才知道 SMACK 站在哪个位置2.1 一个钩子点遍及内核的“门卫检查站”先别急着上手敲命令我建议你把 LSM 框架的基本形态搞清楚否则后面配置 SMACK 标签时你会总觉得“这规则咋有点像防火墙规则但又不太一样”。LSM 的设计思路可以理解成在内核的关键路径上安插了一排“门卫检查站”。比如某个进程要打开文件、要创建子进程、要发送网络数据、要挂载文件系统——这些都是高危动作内核对每个动作本来就有自己的权限检查逻辑。LSM 做的是给这些动作加了一道“额外的安检门”。内核在每个关键操作前后预留了回调函数接口LSM 模块通过注册自己实现的钩子函数把安全检查逻辑挂上去一旦检查不通过这个操作直接就被拦截了。比如file_permission、inode_permission、task_alloc_security这类钩子分别管理文件访问、inode 访问、进程创建等环节。SMACK 就是实现了这一组钩子函数的一个内核模块。它在这些钩子里做的核心事情就是“读标签 - 比对规则 - 给出允许或拒绝的结论”。这里有个关键认知Linux 内核从早期就不断有安全增强补丁后来把这些补丁统一化、框架化就成了 LSM。早期模块化的安全方案实现各不相同互相之间很难兼容LSM 的功劳在于把这些能力抽象成统一接口。现在内核里你可以看到多个安全模块共存例如 capabilities、SELinux、AppArmor、SMACK、Yama 等。2.2 为什么选了 SMACK 而不是 SELinux 或 AppArmor我接触过的工程师一提到强制访问控制下意识就想到 SELinux主要用于桌面和服务端和 AppArmor主要面向 Ubuntu 体系。但它们在嵌入式、物联网设备、工业控制场景下其实都有不太顺手的地方。SELinux策略语言非常强大能表达细到“某个类型的进程只能对某个类型的文件进行某种操作”。但强大伴随的是极端复杂的策略编写动不动就是几百行的.te文件。我见过不少团队最后应付式的用permissive模式跑等于自我安慰。策略写错还会导致系统异常排错过程很容易让人崩溃。AppArmor它走的是路径配置思路主要保护特定程序配置相对简单。但对于整个系统层面做统一强制管控粒度和灵活性都欠点火候更别说它本身对进程间通信、网络标签这种场景的支持比较弱。SMACK初衷就是“简化版强制访问控制”。它用一套非常直观的标签体系每个人物、文件等实体上挂一个字符串标签规则就是三元组“主体标签 客体标签 访问权限”。你不需要写复杂的策略规则文件所有规则实际上都是简单的文本记录一眼能看懂。当时选 SMACK 还因为它在内核社区里属于轻量级实现对系统性能开销小特别适合资源敏感的嵌入式系统。规则少时它的权限判断就是字符串比对加 hashtable 查找开销不算大。当然SMACK 也有局限它不像 SELinux 有那么丰富的 type enforcement、角色层次、多级安全策略。它更倾向于“给东西贴上标签然后按标签做门禁”属于强制访问控制但偏简单实用的一支。这个定位决定了它在通用服务器上不如 SELinux 流行但在特定场景下反而正好契合需求。2.3 三个核心概念标签、规则、主体客体SMACK 的世界里有三个绕不开的基础概念标签Label一段不超过 255 字符的字符串可以挂在文件通过扩展属性security.SMACK64、进程通过/proc/pid/attr/current、网络 socket 等对象上。标签是身份标识身份决定权限边界。主体与客体主体通常是进程客体是文件、目录、网络对象等资源。进程的标签决定了这个进程发起访问时的“身份”客体标签则是资源本身的“身份”。访问发生时主体标签、客体标签、请求的权限三者组成一条查询去匹配系统的规则集合。规则Rule规则由三段组成主体标签 客体标签 权限字符。权限字符有r读、w写、x执行、a追加等。比如ProjectA SharedData r就表示标签为 ProjectA 的主体对标签为 SharedData 的客体拥有读权限。这个设计跟防火墙规则很像只不过防火墙匹配的是 IP 和端口SMACK 匹配的是标签和权限。正因为它简单所以你可以把规则像配置表一样管理起来而不是像写程序一样写策略逻辑。3. 深入 SMACK 的规则与访问判定逻辑3.1 默认规则你不知道的那些“兜底行为”SMACK 能不能用得顺手关键看你对它的默认判定逻辑熟不熟。它在空规则环境下也有一套兜底机制我直接把内核里的默认行为给你们捋出来如果主体的标签和客体的标签完全相同默认允许访问。这个很好理解同类之间互不设防。如果客体标签是_下划线DMZ 或称“通配标签”允许任何主体访问“仅被授予1类权限”的资源。实际上_在 SMACK 规则被映射为一个透明标签常用于临时/公共数据区。如果主体标签为*星号通常指特权标签默认几乎允许访问所有对象。这在系统启动早期或初始化进程中使用相当于一把“万能钥匙”。如果客体标签是^hat 标签表示不允许执行exec操作。特殊标签如表示一个进程的“网络标签”用于网络通信场景。说真的这些特殊标签其实特别容易懵一开始我记反了_和*的含义导致配置出来的系统该放行不放行不该放行的还放行。后来自己归纳成一句话*是特权主体标签_是公共客体标签^是禁止执行标签。注意这里说的默认规则也会因内核版本和配置不同而有差异。我在新内核上测试时还发现过行为差异的细节所以别把默认规则当成“永远不变的默认值”关键时刻还得看内核文档。3.2 规则匹配流程与 onlycap 的影响SMACK 的规则匹配不复杂但它不是一个“找到即完事”的过程它是按优先级递进判断的。我自己实现过一次简化版的规则匹配逻辑大致是先检查主体标签是否等于客体标签。是直接允许同类之间互信。检查规则集中是否有标准的显式规则subject object perms。如果有且权限匹配则按规则执行如果没有进入下一步。检查 onlycap 机制如果主体标签在onlycap列表中则只有在规则集内明确允许时才放行相当于“特权进程也必须在白名单内”。默认拒绝。你可以看到SMACK 虽然简单但它也有一个 “onlycap” 的概念它允许你定义一组“能力受限的标签”让这些标签即便有 root 权限也无法绕过 SMACK 检查。这一点非常关键普通 DAC 权限下 root 是能改一切的但 SMACK 的检查独立于 DAC且在内核中执行所以即使 root 用户也无法绕过 SMACK 的限制除非你有特定 privileged label。我曾在项目里用 SMACK 把某个服务进程的标签设为RestrictedDaemon然后把该标签加进 onlycap。然后即使服务以 root 身份跑它想读取其他标签下的敏感文件也只有规则内允许的部分路径可走。这种“root 也被卡住”的效果是传统 Unix 权限完全做不到的。3.3 常用命令与接口加载规则、打标签、查状态上手 SMACK 只要熟悉几个命令和文件节点很多资料把这部分写得过于零散我按实际项目中高频使用的顺序整理一份操作命令或接口说明查看当前内核是否启用 SMACKdmesg | grep SMACK或检查/sys/kernel/security/lsm文件/sys/kernel/security/lsm会列出已启用的 LSM 模块名查看当前进程标签cat /proc/self/attr/current返回进程的安全标签设置进程标签写/proc/pid/attr/current或用smacklabel工具需要相应权限通常需 root 或在某个 privileged label 下查看文件标签getfattr -n security.SMACK64 /path/to/file读取文件的 SMACK 扩展属性设置文件标签setfattr -n security.SMACK64 -v LabelName /path/to/file给文件打上标签加载规则写/sys/fs/smackfs/load2或用smackload每行一条规则格式subject object permissions加载 onlycap写/sys/fs/smackfs/onlycap写入一个标签将使其加入 onlycap 列表查看规则cat /sys/fs/smackfs/load2列出当前加载的规则可看到全部显式规则这里特别注意打标签时用的是文件扩展属性所以支持扩展属性的文件系统才能完整使用 SMACK。如果是 tmpfs、ramfs 这类不支持的场景需要额外的配置或换成 smackfs 管理的目录来加载标签。我在嵌入式系统的 rootfs 里一般会确保使用 ext4 或 squashfs配合security.smack64编译选项等支持 xattr 的文件系统。4. 从内核编译到规则落地SMACK 完整实操4.1 准备内核开启编译项和启动参数要在自己的系统上用 SMACK第一步当然是内核支持。这里有两种情况一种是直接用发行版内核部分发行版编译了 SMACK 模块另一种是自己编译内核。自己编译时需要确保如下几个配置开启CONFIG_SECURITYyCONFIG_SECURITY_SMACKy如果你不想把 SMACK 编死在内核里也可以设为m以模块方式加载但考虑到安全模块的加载顺序很关键我建议你直接编进内核。一台系统在启动阶段就需要安全标签模块加载太晚会出现标签缺失的窗口期。启动参数里通常不需要额外指定但如果你的系统同时启用多个 LSM为了保证 SMACK 处于启用列表可以显式在启动参数里加入lsmsmack或者lsmsmack,capability,yamalsm参数控制安全模块的启用顺序和集合。顺序为什么重要因为有些管理节点如/proc/self/attr/current在多个模块启用时只绑定到最先启用的模块。如果你想断言“当前进程的标签是 SMACK 标签”最好让 SMACK 排在安全模块顺序的前面。4.2 初始化脚本先建立基础标签和初始规则系统启动后SMACK 会自带一些基础的root标签和_标签相关的初始化逻辑。但真实环境中你肯定要在服务拉起之前便把标签和规则准备好。否则你可能会遇到启动进程被错误拦截的问题。我习惯把它拆成一个初始化脚本按顺序做这几件事挂载 smackfsmount -t smackfs smackfs /sys/fs/smackfs给文件系统关键路径打标签。比如我给/etc打上SystemConfig标签给/usr的可执行文件打上SystemBin标签把业务数据目录/data/app1打上App1Data标签。加载基础规则。这里的规则要覆盖系统启动所需的最小权限否则系统会在初始化阶段就崩溃。常见的基础规则包括* SystemConfig r * SystemBin rx SystemDaemon SystemConfig r SystemDaemon SystemBin rx这里的*表示特权主体理论上能访问大多数客体所以用它给 root 早期阶段保底是合理的。但后续服务标签一定要收敛不要全程用*。将不必要的特权标签加入 onlycapecho SystemDaemon /sys/fs/smackfs/onlycap需要注意写入 onlycap 后标签为 SystemDaemon 的进程即使有 root 的 DAC 权限也不能随意访问未授权的 SMACK 客体。这一步我测试过能有效防止守护进程因漏洞被利用后直接读取系统任意文件。4.3 用标签隔离业务模块一个多租户模拟场景为了把 SMACK 实际的效果展示清楚我搭过一个模拟场景一个嵌入式设备上同时运行两个业务模块 App1 和 App2它们的数据都存放在设备本地目录二者不能互相访问彼此的数据目录但都要能读公共配置目录。第一步准备目录/data/app1 /data/app2 /data/shared第二步打标签setfattr -n security.SMACK64 -v App1Data /data/app1 setfattr -n security.SMACK64 -v App2Data /data/app2 setfattr -n security.SMACK64 -v SharedData /data/shared注意目录的标签会默认传递给其下新建文件如果挂载选项和文件系统实现支持但不保证覆盖后续通过其他方式创建的文件。稳妥起见我通常在应用启动前用脚本统一检查并补齐新文件的标签。第三步加载规则App1 App1Data rw App2 App2Data rw App1 SharedData r App2 SharedData r App1 App2Data - App2 App1Data -其中-表示无权限显式拒绝。虽然默认情况下没有规则也会拒绝但写出显式拒绝规则能让后续排查规则时更直观一眼就知道意图。第四步分别以 App1 和 App2 标签启动两个服务进程例如echo App1 /proc/self/attr/current 2/dev/null || smacklabel App1 /usr/local/bin/app1如果你的系统没有配置允许当前 shell 进程修改标签可能要借助一个小工具或通过服务启动器统一处理。4.4 验证隔离效果眼见为实配置完成后我通常在两个服务里各开一个落盘测试App1 尝试写/data/app2/test.txtApp2 尝试读/data/app1/secret.txt。实测结果很干脆——App1 写 App2 目录直接返回权限错误代码里open返回-EACCES日志里能看到Permission denied。而 App1 读/data/shared/config.ini完全正常App2 也一样。这说明 SMACK 的规则真实生效不是摆设。还有个小细节如果你用 root 身份去手动执行cat /data/app1/secret.txt在 SMACK 启用且 root 进程标签没有特殊权限的情况下照样会被拒绝。我第一次测试时还挺惊讶的后来想通了SMACK 的强制访问控制独立于 uid/gid 的 DAC 检查且 LSM 的检查发生在 DAC 检查之后或并行所以“root 不能为所欲为”在 SMACK 下是真实效果。对做安全方案的人来说这反而是亮点。4.5 与网络标签的结合SMACK 不只管文件和进程它还能管网络。通过给 socket 打上标签结合 netfilter 或 SMACK 自带的网络访问控制逻辑通过SECMARK或cipso可以控制哪些标签的进程能访问哪些网络资源。这块比文件标签要稍微绕一点需要同时设置SO_PEERSEC相关的套接字选项或者利用netlabel工具来配置 CIPSOCommon IP Security Option标签。我还没在所有场景下都用它做过网络隔离但至少能确定一点对于想控制本地进程访问外部指定网络的场景SMACK 网络标签是值得试的路线之一。它跟 iptables 的区别是iptables 按 IP/端口过滤SMACK 还能带上进程身份标签一起过滤控制语义更贴近业务。5. 调试与常见坑我在 SMACK 上踩过的那些问题5.1 常见问题速查表现象可能原因解决思路应用启动直接被拒绝但 DAC 权限正常缺少相应的 SMACK 规则检查/sys/fs/smackfs/load2对比应用标签和文件标签补规则明明配了规则还是Permission denied标签打错了可能有空格或用错扩展属性名用getfattr -d -m -查看所有安全属性确认security.SMACK64值服务进程标签显示_进程启动时未设置标签或标签丢失检查启动器逻辑在 exec 之前写入标签文件新创建后标签不正确父目录标签未正确继承或因文件系统不支持 xattr确保 rootfs 使用支持扩展属性的文件系统或通过smack_setaccess类工具补充系统启动过程卡死或服务起不来初始化脚本加载规则太晚把 smackfs 挂载和规则加载提前到服务启动前使用 initramfs 时也要提前加载root 也被拒绝当前 root 标签非特权标签或 onlycap 限制确认 root 进程标签是不是*或检查 onlycap 列表这里啰嗦一句常见坑第一名就是标签错位。因为标签本身只是一个字符串它不像 IP 地址那样有格式校验你打标签时多敲一个空格、大小写不一致表现出来的都是同一个Permission denied非常消耗排查时间。所以建议从一开始就把所有标签用一个常量表管理起来脚本也好配置生成器也好统一从一处取标签名。5.2 排错三板斧dmesg、smackfs、permission 测试我的排错方法很固定分三步看dmesgSMACK 在拒绝访问时经常会在内核日志里输出一行包含标签信息的提示比如SMACK: process label[App1] obj label[App2Data] ...。这行日志几乎直接告诉你是哪条规则缺失了。核对规则集用cat /sys/fs/smackfs/load2输出当前规则用脚本比对目标判断是不是有缺失规则。甚至可以写一个小脚本把标签组合全景拉出来自动找出没有对应规则的组合。做最小复现单独写一个小测试程序只执行一个文件打开操作逐步加上规则确认哪一步放行、哪一步拒绝。这个方法虽然笨但能快速定位是标签问题还是规则问题。5.3 与原生命令的兼容问题有个容易让人困惑的地方是ls -l显示的传统权限位和 SMACK 访问控制之间的冲突。比如传统权限位显示rwxr-xr-x但应用就是读不了文件。这是因为传统 DAC 检查过了但 SMACK 的 LSM 检查拦住了。当你发现“权限看着没问题但实际操作失败”时一定要想起来去查安全标签和 SMACK 规则。此外容器场景下 SMACK 和容器运行时比如常见的 runc之间的协作也是坑点。有些容器运行时默认会操作 capabilities、cgroups但不会处理 SMACK 标签。结果容器进程的标签沿用宿主机继承下来的标签隔离效果可能完全不符合预期。如果你要在容器里用 SMACK需要针对容器的 init 进程、挂载目录、网络命名空间都打上设计好的标签并额外配置 runc 的 security context 参数。5.4 一劳永逸的小技巧把 SMACK 规则纳入配置管理由于 SMACK 规则本质上就是文本特别适合纳入版本管理。我建议用类似这样的结构来组织规则文件# 主体 客体 权限 * SystemBin rx * SystemConfig r App1 SharedData r App2 SharedData r App1 App1Data rw App2 App2Data rw然后在启动脚本里通过循环批量加载while read -r subj obj perm; do [[ $subj ~ ^#.* ]] continue echo $subj $obj $perm /sys/fs/smackfs/load2 done /etc/smack/rules.conf这样可以避免手敲大量命令。我觉得 SMACK 比 SELinux 容易推广的一个重要原因就是规则文件简洁可以像防火墙规则一样用文本管理不需要专门学习一种策略语言。6. 实操心得与后续扩展SMACK 这套方案我在实际项目上跑了有大半年整体感觉是如果你能接受“标签即身份”这种设计哲学那它在嵌入式设备、专用服务器、容器隔离等场景下确实能提供一种性价比很高的强制访问控制方案。我个人在实际操作中的体会是SMACK 最大的价值不在于它多强大而在于它“把安全检查点分散到了内核各处却把配置复杂度控制在了人力可维护的范围”。相比之下我曾经帮人看过一个 SELinux 策略文件光类型定义就 2000 多行新同事根本不敢改。SMACK 的规则文件能在一页纸里写完这本身对安全策略的可持续维护就是一种隐性收益。如果你准备在自己的项目里尝试 SMACK我建议从最小系统开始做验证先只给两个目录和两个服务打标签把规则加通再逐步扩展。千万不要一开始就规划几十个标签、上百条规则那样容易出现“规则之间互相重叠、自己也搞不清到底放开到哪一层”的失控局面。最后再分享一个小技巧打标签时尽量统一使用“语义化命名”比如用WebAppData、WorkerProcess这种一看就懂的名字。安全策略的可读性太重要了因为一旦系统跑起来来维护的人大概率不是当初写策略的哪一个人。标签命名清楚规则维护成本能低一半。SMACK 还有不少扩展点比如 CIPSO 网络标签、与审计子系统配合做安全审计、在容器运行时做细粒度沙箱。这些方向我都只是摸了个边但如果你已经能用 SMACK 管好文件和进程那再往网络和容器方向扩展时会顺很多。说白了它就是一套“用最简单的方式把内核查不到的地方补上”的机制后续能玩出多少花活全看你怎么定义标签和规则了。