恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SELinux与SEAndroid:从DAC到MAC,深入理解Linux强制访问控制与avc: denied排查
首页
资讯中心
/
SELinux与SEAndroid:从DAC到MAC,深入理解Linux强制访问控制与avc: denied排查
SELinux与SEAndroid:从DAC到MAC,深入理解Linux强制访问控制与avc: denied排查
发布时间:2026/10/10 18:16:12
遇到过一个挺诡异的现象吗Linux 服务器上你明明是 rootcat一个文件却提示Permission denied或者 Android 手机上已经给应用开了存储权限它读自己的数据目录里的文件还是失败。很多人第一反应是“权限坏了吧”实际上多半不是而是你的系统里除了传统权限之外还有第二套锁——SELinux 的 MAC 强制访问控制。Android 上叫 SEAndroid底层原理一脉相承。这篇文章从 DAC 的边界说起把 SELinux 的标签世界、TE 规则、SEAndroid 的特殊之处以及一条avc: denied日志怎么从出现到解决完整走一遍。适合 Linux 运维、Android BSP 工程师、嵌入式开发还有那些总在面试题里看到“DAC 和 MAC 区别”却一直没搞透的人。1. 从 chmod 777 说起DAC 那套规则的边界在哪里1.1 传统权限模型属主说了算也止步于属主Linux 给人最直观的权限概念就是ls -l那一串rwxr-xr-x还有chmod、chown。这套东西叫 DACDiscretionary Access Control自主访问控制。“自主”两个字是关键文件的属主有权力决定谁能访问这个文件你可以把文件权限改成777让全世界读也可以改成600只留给自己。整个判断逻辑很简单——看当前用户的 uid/gid再对照文件的属主/属组/其他三类位能匹配上哪一类就用哪一类的权限。这套模型在单机、单用户的时代够用但它天生有几个短板。第一个是权限语义太粗读、写、执行就这三个动作。可现实里“读”和“读”不一样能read一个配置文件和能把这个文件内容通过网络发出去在 DAC 里根本没区别。第二个更致命的点是文件属主可以把权限“转让”出去一旦某个文件被粗心地设成777哪怕是临时目录里一个不起眼的小文件系统里任何进程都能读。大多数人都以为“权限收紧靠 chmod 就够了”实际上 DAC 管得住的是“这个文件谁能碰”管不住的是“拿到访问权之后那个进程到底想干什么”。1.2 root 与 capability补丁式的权限拆分拆不出安全感DAC 还有一个大 Bossroot。root 在 DAC 模型里几乎不受限rwx对 root 来说基本是形同虚设chmod 000的文件 root 照样能读。这带来一个很现实的场景——如果你的 Web 服务被注入、被提权成了 root那么/root/.ssh/id_rsa、数据库备份、其他用户的/home目录统统对恶意进程敞开。DAC 完全防不住这种“合法身份下的非法操作”。后来 Linux 搞了个改进叫 capabilities把 root 的大权限拆成几十个小权限位比如CAP_DAC_READ_SEARCH、CAP_NET_BIND_SERVICE进程可以只拿一部分能力运行。这确实比“要么 root 要么普通用户”精细了但它本质上还是“按进程拥有的特权集合来判断”它管不了“一个 uid1000 的普通进程能不能读另一个 uid1000 的进程刚写出来的 private 文件”——uid 相同DAC 就默认放行。这种“只要身份对得上就信任”的思路就是 DAC 所有漏洞的源头。1.3 我实际见过的一个 DAC 失效场面以前调一个嵌入式设备里面跑了一个采集程序会产生临时文件放在/tmp下为了方便调试一直用的777。后来发现设备上另一个本来无关的进程也能读到这些数据而且还能删。查了半天才发现问题不在业务逻辑就是/tmp的777加上“同 uid 进程互相可见”把数据暴露了。事后用 DAC 的视角看这套机制从设计上就没打算防这个——它只关心“你有没有权限碰这个 inode”不关心“你碰了之后合不合规矩”。所以说DAC 是系统安全的第一道门但绝对不是最后一道门。2. MAC第二个守门员如何堵住 DAC 的漏洞2.1 强制访问控制的核心逻辑策略由系统说了算MACMandatory Access Control强制访问控制和 DAC 最大的区别一句话就能讲明白DAC 是“文件的属主说了算”MAC 是“系统的安全策略说了算”。在 MAC 模型里任何主体进程访问任何客体文件、socket、设备节点等都要经过统一的安全策略检查。这个策略不是普通用户能改的普通用户哪怕文件权限设成777也不代表进程就能随便碰——还得看 MAC 层放不放行。SELinux 就是 Linux 下最主流的 MAC 实现它用自己的规则在 DAC 检查通过之后再查一遍。俩守门员是串联关系DAC 先放行了SELinux 还会再拦住你DAC 不放行的SELinux 也救不了你。很多人在avc: denied日志出现时第一反应是“chmod 777 一下”结果发现没用就是这么回事——你在调的是第一道门拦你的是第二道门。2.2 一张表说清 DAC 与 MAC 的核心差别对比维度DACMAC / SELinux决策依据对象的属主、属组、其他位uid/gid rwx主体标签、客体标签、操作类型type/class/perm谁制定规则文件属主可通过 chmod 自主修改系统管理员统一配置策略普通进程不可变控制粒度读、写、执行三档细到 file/socket/ipc/binder 等上百个类别的具体操作对 root 的约束基本不约束同样受约束防御重点防止“无关用户”访问文件防止“被攻破/被提权的进程”越权访问资源典型案例所有 Linux 发行版默认权限SELinux、SEAndroid、AppArmor这个表格基本能拿来当面试题的提纲。DAC 管“你是谁”MAC 管“你做了什么事儿”。这俩不是替代关系是叠加关系。2.3 MAC 的“强制”到底强制在哪儿很多第一次接触 SELinux 的人会被“强制”两个字吓到以为是限制了用户自由。实际上它强制的是“所有访问都必须查策略”这件事。也就是说哪怕你是一个合法登录的管理员通过 SSH 登录后想读一个文件SELinux 也会检查你的 shell 进程的标签和文件标签是否匹配。这种强制不是针对某个人而是针对系统里所有的客体交互。在 SELinux 里受控的操作远不止文件读写还包括网络端口绑定、socket 的连接、进程间信号、共享内存、目录遍历、设备节点访问甚至 Android 里的 Binder 调用。一个刚接触 SELinux 的人最容易低估的就是这个“粒度”。你给某个进程配了文件读权限它可能在 socket 上发起连接时又被拦然后你又得去找对应的 class。这种一粒一粒补齐权限的过程其实就是用“麻烦”换“安全”。3. SELinux 的标签世界Type Enforcement 是怎么工作的3.1 万事万物皆有标签SELinux 的基本要素SELinux 的判断基础是标签label。每个文件、每个进程、每个设备节点都带有一个安全上下文格式是user:role:type:level。例如system_u:object_r:etc_t:s0 system_u:system_r:init_t:s0实际排查时最常看的只有一个字段——type第三段。进程的 type 习惯上叫 domain域比如init_t、httpd_t文件、设备等客体的 type 叫类型比如etc_t、var_log_t。SELinux 的策略核心就是一组组规则用来声明“哪个 domain 可以访问哪个 type 的哪类对象”这就是 Type Enforcement类型强制TE。看一个最典型的 allow 规则allow httpd_t httpd_sys_content_t:file { read open getattr ioctl };意思是运行在httpd_t域的进程允许对类型为httpd_sys_content_t的文件执行read、open、getattr、ioctl这四个操作。注意file是对象类别class后面的花括号里是权限集合。如果这条规则不存在或者权限集合里没有你要的那个操作SELinux 就会拒绝访问并在审计日志里记一条avc: denied。3.2 默认拒绝SELinux 的思维模式和防火墙一样SELinux 的策略模型默认是“白名单”——没有明确允许就是拒绝。这一点和 iptables 的默认规则思路很像默认 DROP 还是默认 ACCEPT决定了安全基线。SELinux 默认 deny 的做法让系统拥有了一个很硬的安全底子哪怕 bug 被利用进程想“顺手”做点规则之外的事情也会被挡在策略墙外。这也解释了为什么很多系统一开 SELinux各种服务开始出现“莫名其妙”的权限问题。不是权限坏了而是你之前的行为在“无策略”状态下本来就失控现在有了默认拒绝把平时靠 DAC 悄悄放行的行为全部暴露出来了。理解了“默认拒绝”这个心智模型再看 SELinux 的部署困境就好办了你要做的不是抱怨它挡路而是把业务真正需要的访问路径一点点显式声明出来。3.3 enforcing 与 permissive调试时的两个模式SELinux 有 Enforcing强制、Permissive宽容、Disabled关闭三种状态。Enforcing 下违规操作会被直接拒绝Permissive 下违规操作不会被阻止但会照常记录日志。这两个模式之间的切换就是排错时最重要的调试手段。查看和修改当前模式# 查看当前模式 getenforce # 临时切换为 permissive重启失效 setenforce 0 # 临时切换为 enforcing setenforce 1 # 永久修改写入 /etc/selinux/config # SELINUXenforcing我调试 SELinux 问题时第一步永远是把目标域或全局切到 permissive然后复现问题观察日志。通过日志确定“到底需要哪些规则”再回到 enforcing 模式验证。直接关 SELinux 是最粗暴的方式它会让异常行为在没有任何记录的情况下继续发生排查起来反而更困难。生产环境我从来不用 permissive因为它只是把问题藏起来不会让问题消失。3.4 两个流传很广的误解第一个误解是“开了 SELinux 就安全了”。SELinux 是一个机制不是一劳永逸的保险。如果策略本身写得过于宽松比如大量allow xxx_t all_t:file *那等于没开。第二个误解是“SELinux 可以防黑客”。它防的不是入侵而是入侵后的横向扩散——一个被拿下的进程即使攻击者拿到了 shell也没法随意读取系统里其他敏感标签的资源。也就是说SELinux 的价值不在于“不让坏人进来”而在于“坏人进来了也带不走东西”。4. SEAndroidDAC 与 SELinux 在移动端的叠加实现4.1 为什么 Android 宁可折腾也要全面启用 SELinuxAndroid 在 4.3 时代开始引入 SELinux5.0 开始全面 enforcing。原因是移动应用生态太复杂了上万个互不信任的应用跑在同一个 Linux 内核上如果只靠 DAC 的 uid/gid 隔离一旦某个应用拿到更高权限比如通过漏洞提权到 system 或 root就能直接读其他应用的数据、改系统配置、控制硬件。这种场景下DAC 基本等同于裸奔。SEAndroid 把 SELinux 的模型搬到了 Android 上而且做得更严格。它给每个 app 定义了自己的 domain比如untrusted_app、platform_app、system_app并限制它们能访问的 type。一个普通的第三方应用即使通过漏洞突破了 uid 沙箱在 SELinux 这一层也过不去——策略根本不允许它访问其他应用私有目录下的 data 文件。这种纵深防御的设计让 Android 在大量提权漏洞面前仍然保持了不错的安全性。4.2 sepolicy、te 规则和 neverallowAndroid 的策略骨架SEAndroid 的策略文件依然以.te结尾语法和 SELinux 一致。在 AOSP 里你能看到一堆*/sepolicy/*.te文件里面定义了系统和各模块的 domain、type、allow 规则。比如allow untrusted_app app_data_file:file { read write open getattr };在 Android 策略里有一个补充机制叫neverallow。它表示“哪怕有 allow 规则这条访问也绝对不允许”并且会在编译策略时做静态检查。也就是说如果某个.te文件试图允许untrusted_app访问system_server的 data编译直接报错根本进不了系统。这个机制的价值在于它把安全边界从“运行时的策略检查”提前到了“编译期的属性检查”让厂商在定制系统时更难把安全策略改坏。4.3 SEAndroid 独有的控制对象不只是文件SEAndroid 在 Linux SELinux 的基础上加入了 Android 特有的对象类别。比较典型的是binder操作、Android Property系统属性和 Service系统服务。Binder 是 Android 进程间通信的主要方式SEAndroid 会检查调用者和被调用者的标签是否允许建立 Binder 连接这能阻止恶意应用调用系统服务的高危接口。属性服务也有专门的property_contexts文件用来定义每个属性的访问权限不是随便一个进程都能改ro.*、persist.*这类全局属性。做系统开发时常见的是 debug 版本跑起来后 logcat/dmesg 里刷屏的avc: denied。比如自定义的 native 服务想访问/data/local/tmp下的文件或者想读某个 device 节点。这时候光看 DAC文件 uid/gid/权限是不够的还要检查file_contexts是否给路径定义了正确的标签以及system_app.te或其他 domain 的 te 文件是否允许访问对应 type。Android 平台上的 SELinux 调试本质上和服务器上是同一套方法论差别只在于你要面对的对象列表更长、更杂。4.4 Android 设备上快速看 SELinux 状态# 查看全局状态 adb shell getenforce # 切换模式需要 root adb root adb shell setenforce 0 # 查看某进程的标签 adb shell ps -Z # 查看某文件的标签 adb shell ls -Z /data/local/tmp在产品化之前我习惯在 userdebug 版本上把所有自研模块过一遍把每个avc: denied都清零再切回 enforcing 做回归。这个步骤省不了因为很多问题只在高压力、多线程并发时才会暴露出缺少的权限userdebug 上不跑透上线之后再发现就是事故了。5. 实战一条 avc: denied 日志的完整排查链路5.1 日志从哪里来长什么样SELinux 被拒绝的事件会记录在审计日志里。服务器上通常看/var/log/audit/audit.log或者用ausearch -m avc -ts recent查最近记录。Android 上则是看 dmesg 或 logcat。一条典型的日志长这样[ 1234.567890] audit: avc: denied { read } for pid1234 commmy_daemon namednsmasq.conf devmmcblk0p25 ino12345 scontextu:r:myapp_domain:s0 tcontextu:object_r:system_file:s0 tclassfile permissive0先别慌这段信息足够定位问题。核心是三个字段scontext源上下文也就是发起操作的进程标签看第三段的 domain这里是myapp_domain。tcontext目标上下文被访问对象的标签这里是system_file。tclass对象类别这里是file也可能是dir、sock_file、binder等。{ read }是被拒绝的操作类型permissive0表示当前是 enforcing 模式这次是真的被拦了如果permissive1说明当时在宽容模式下操作没有被真正阻止只是记了一笔。5.2 从字段到策略两条路线的思考方式拿到日志后不要急着写规则。先回答两个问题第一这个进程应不应该访问这个对象第二如果应该缺的是哪条策略第一个问题是业务判断第二个是技术判断。业务判断经常被人跳过。我见过有人对着avc: denied一通操作最后发现那个访问本来就是不该发生的——比如一个本该只读配置的进程在尝试写/data下别的应用的目录这属于 bug 而不是策略缺失。遇到avc: denied先想想这个行为是否合理。如果合理再进入技术路线明确 scontext 的 domain 是哪个、tcontext 的 type 是哪个、tclass 和 permission 是什么然后去对应的.te文件里补规则。5.3 三步排错法permissive 化、audit2allow、最小化裁剪完整排错链路我总结成三步。第一步把目标 domain 先置为 permissive复现问题收集足够日志。临时把单个 domain 放进 permissive可以在不改大全局的情况下放开对某个域的限制同时让它继续产生avc记录。在 Android 上典型的做法是用adb shell setenforce 0切全局 permissive但这样太粗容易把系统里所有问题全部暴露出来。更好的做法是先保持全局 enforcing只对目标进程的 domain 做临时调整通过修改 sepolicy 或使用setsebool控制可用布尔值。有些 Android 调试版本也支持按 domain 配置 permissive。原则是导致目标问题的进程进 permissive其他区域保持 enforcing。第二步收集日志导出规则草稿。在 permissive 模式下跑完复现流程后收集这段时间内该 domain 被拒的记录ausearch -m avc -ts recent -c my_daemon然后用audit2allow生成规则草稿ausearch -m avc -ts recent -c my_daemon | audit2allow它会输出类似这样的建议allow myapp_domain system_file:file { read open getattr };第三步人工裁剪规则验证后固化。千万不要把audit2allow的输出原样贴进策略。它给出的规则往往权限偏宽你要对照日志一条条判断这个访问是否确有必要权限集合里是不是只有read和open就够了write其实是误报目标 type 是否应该换成一个更贴近业务的 type比如不该直接用system_file而应该用你自己定义的myapp_data_file做完了裁剪编译进策略再切回 enforcing复现原问题场景确认不再有新的avc: denied。最后在.te文件里保持注释写清楚“为什么加这条规则、对应哪个需求”方便三个月后的自己或同事接手。5.4 排错时我踩过的几个大坑第一个坑dontaudit 规则太多导致日志“消失”。系统里经常有大量dontaudit规则专门用来抑制无害拒绝的日志。如果某条访问正好被 dontaudit 盖住了你在 dmesg 里根本看不到任何记录容易误以为“权限已经够用”。这时候可以用semodule -DB关闭 dontauditAndroid 上可以通过修改 policy把被抑制的拒绝记录重新暴露出来再继续排查。第二个坑permissive 跑了几天都没日志一切换 enforcing 就挂了。原因大概率是你复现路径太短很多权限在特定时序下才会触发。比如开机时某个模块初始化没问题但热插拔、重连、状态切换时才会去访问另一个节点。调试时必须把完整的功能路径都走一遍不能只走 happy path。第三个坑只改了 allow 规则没改文件标签。SELinux 判断的是标签不是路径你的进程访问/data/vendor/my_service被拒如果file_contexts里根本没给这个路径定义 type那它就继承父目录的标签可能是vendor_data_file而不是你以为的my_service_data_file。你加了一条allow my_app my_service_data_file:file read;结果实际访问的 type 是vendor_data_file当然还是被拒。所以排错时先用ls -Z确认目标文件的真实标签再决定要改策略还是改file_contexts。6. 进阶让 SELinux “不挡路”又不失保护的边界做法6.1 用布尔值和本地策略做“受控放行”SELinux 提供了一个比直接改 allow 规则更优雅的机制布尔值boolean。系统里内置了一堆布尔值专门用于在运行时开关某些行为比如httpd_can_network_connect、samba_export_all_rw。这些布尔值可以通过setsebool -P永久修改不需要编译策略也不影响大局策略的完整性。setsebool -P httpd_can_network_connect on这种方式适合线上系统快速调整但不要滥用。布尔值能覆盖的场景比较有限更细的权限控制还是要走定义 type、写 allow 规则的路子。布尔值是“系统预留的开关”不是“万能借口的钥匙”。6.2 容器场景下 SELinux 的脾气在 Docker/Podman 这类容器环境下SELinux 的标签逻辑会带来一个比较经典的坑挂载宿主机目录进容器后容器里的进程读不了文件。原因是宿主机文件的标签和容器 domain 不匹配。解决办法是给挂载卷打上svirt_sandbox_file_t标签挂载时使用:Z或:z选项让容器运行时自动调整标签。:z表示共享标签多个容器可以用:Z表示私有标签只能当前容器用。podman run -v /data:/data:Z my-image这个细节在嵌入式 Linux 上也会遇到比如 systemd-nspawn 启动的容器、甚至用 chroot 隔离的进程如果宿主开启了 SELinux容器内进程访问宿主机挂载目录时同样会被拦截。解决思路一致——要么给挂载目录打合适标签要么在策略里声明对应的允许规则。6.3 对策略维护的长期心得把时间跨度拉长SELinux 策略的维护比想象中更需要纪律性。我见过系统里慢慢堆了几百条 allow 规则很多都只剩下“后人不知为何添加”的状态。对此我有几个建议每条自定义规则都要带注释写清楚业务场景和联系人。定期用audit2allow跑一遍实际日志把已经用不到的老规则清理掉。内核或 Android 版本升级后务必做一次完整的 SELinux 回归测试因为新版的策略和对象类可能有变化。策略文件本身也要走代码评审。格式、拼写、命名风格这些看着无所谓的问题在团队协作时会影响可维护性。另外我个人的一个体会是与其抱怨 SELinux 增加工作量不如把它当作一套强制的“最小权限审计工具”。每一次avc: denied其实都在提示你——你的进程又在尝试做超出职责范围内的事。把这些提示逐条解决的过程本身就等于把系统的权限边界梳理了一遍。这也是为什么我后来调试任何 Linux 或 Android 系统都会先留着 SELinux而不是一键 disable——它逼你把每个角落的访问都看清楚这种确定性是“先关了再说”给不了的。