尧图精选

Linux权限体系详解:DAC、MAC与SELinux/SEAndroid协同排查

🕒 发布时间:2026/10/2 14:38:58 📁 来源:尧图网络
在 Linux 上排查权限问题最折磨人的一件事就是明明ls -l显示文件可读可写属主也没问题程序就是跑不起来。遇到这种情况真正挡住你的往往不是 DAC而是内核里挂着的那层 MAC。我第一次被这俩概念搅昏头是在一台跑 SELinux 的 CentOS 上后来做 Android 系统定制又跟 SEAndroid 打了几年交道。这两个东西都叫“权限管理”但设计思路、错误日志和排查方法完全不同。这篇文章就把 Linux DAC 和 SELinux/SEAndroid 这两套机制拆开讲清楚适合被 SELinux 拒绝日志搞到头大的运维和开发也适合准备 Linux 或 Android 安全面试的读者。知道每一条拒绝背后的“为什么”比背命令更能解决问题。1. 先理清楚DAC 和 MAC 到底在解决什么问题1.1 权限三要素主体、客体与操作任何权限模型都离不开三个东西谁主体、对什么客体、做什么操作。Linux 里最典型的组合是“进程读写文件”主体是进程客体是文件或目录操作是 read/write/execute。你用一个普通用户执行cat /etc/shadowDAC 会先看这个用户有没有读权限SELinux 接着还会看进程的安全上下文和文件的类型是否匹配。光记命令是不够的权限报错时你先要能回答这三个问题发起访问的是哪个进程目标是哪种资源想要执行的动作是什么我自己习惯用一个门禁系统的类比DAC 相当于每间办公室自己定的门锁规则谁有钥匙归屋主说了算MAC 则相当于大楼总部的安保制度就算你有钥匙没通过总部授权系统核心区域照样进不去。两者不是替代关系而是叠加关系。日常看到文件权限正常但操作被拒多半就是第二层安保在起作用。1.2 DAC 的“自主”在哪里体现DACDiscretionary Access Control自主访问控制的核心含义是“资源属主有自主决定权”。文件所有者可以把权限改成 777把文件交给任何人也可以设置 setuid 让普通用户临时获得属主身份。系统默认信任属主的决定内核只负责按当前 uid/gid 匹配权限位。这种设计让日常使用非常灵活你创建的文档你想给谁看就给谁看一个chmod 644就能完成共享。问题是这种信任在攻防场景里是危险的。用户的账号和进程被攻击者控制后攻击者就继承了该用户对资源的所有操作权一旦拿到 rootDAC 几乎全部失效整个文件系统都暴露在读写下。所以 DAC 适合做日常资源隔离但不适合做高强度安全边界。安全团队真正害怕的不是正常用户而是某个服务进程被远程利用后以现有身份横向移动的情况。1.3 为什么需要 MAC当“属主说了算”不够MACMandatory Access Control强制访问控制把权限判定从“跟随属主意愿”改为“跟随系统策略”。哪怕文件的属主是你进程也是你启动的只要系统管理员定义的安全策略不允许这次访问内核就直接拒绝并且产生一条审计日志。SELinux 和 SEAndroid 都是这类机制的落地实现。和 DAC 那种“属主愿意就能授权”的逻辑相比MAC 引入了独立于属主和进程的第三方裁决者。引入 MAC 的核心动机很明确在纵深防御中补上 DAC 的拼图。Linux 服务进程和 Android 应用都经常运行在复杂环境里一旦单个组件被攻破如果没有 MAC 层限制攻击面会迅速扩大有了 MAC即使拿到应用或服务的权限也只能在预设的“域”里活动。这也是为什么现代 Android 从 4.3 开始逐步启用 SELinux至今已经形成一整套应用隔离机制。在服务器端云厂商和操作系统发行版也普遍默认开启 SELinux 或 AppArmor 这类强制访问控制组件。2. DAC 机制核心细节与实操要点2.1 rwx 权限位、粘滞位与 setuid/setgidLinux 文件权限位是 DAC 的基础。每个文件对属主、属组、其他人生成三组 rwx。普通用户想进入目录需要目录的 x 权限在目录里新建文件需要 w读取文件需要文件的 r。这里有一个经典误区只有 x 没有 r 时你能进入目录但列不出内容反过来只有 r 没有 x用 shell 补全时也经常碰壁因为补全要访问目录项必须先有 x。很多脚本部署失败最后查下来就是目录少了执行权限进程连“走进去”的资格都没有。特殊位当中setuid 是最常被攻击者盯上的。像passwd这类命令需要以 root 身份改/etc/shadow所以它带 setuid。如果某个开发在系统里手动给脚本、二进制加了chmod us等于给所有能执行它的用户开了条临时提权通道。尤其要注意给脚本设置 setuid 在大多数 Linux 发行版上是不生效的反而会让人误以为“加了保险”。排查根目录下的 setuid 文件用find / -perm -4000 -type f 2/dev/null粘滞位用chmod t设置典型是/tmp目录对任何人可写但只有属主才能删自己的文件。如果/tmp被误设成普通 777就会出现普通用户删掉别人临时文件的乱子。生产服务器上我还会时常检查 setgid 位因为它直接影响组继承处理不好会让新建文件落到错误属组间接影响其他服务的 DAC 判断。2.2 ACL 与 capabilitiesDAC 的精细化扩展传统 rwx 只能控制属主、属组、其他三类粒度太粗。POSIX ACL 用setfacl/getfacl给单独用户或组授权限适合多团队共享某一目录的场景。例如setfacl -m u:deploy:r-x /srv/app getfacl /srv/app注意ACL 开启后权限位那一列会多一个很多复制工具cp -a或 tar 选项差异不保留 ACL部署时容易静默丢权限。我在自动化发布脚本里吃过这个亏发布机上的目录 ACL 看着正常拷贝到目标机后全部消失服务瞬间失去访问权。后来要求所有部署流程统一用能保留扩展属性的方式并且上线后跑一次getfacl校验。capabilities 则是把 root 的超级特权切成一片片“小能力”。最常见的场景是监听 80 端口传统做法是让程序以 root 启动后再降权或者直接 setuid更合理的是只给它cap_net_bind_service比如setcap cap_net_bind_serviceep /usr/bin/example用getcap可以检查。capabilities 虽然严格来说属于进程特权机制、不直接属于权限位但它和 DAC 配合有效减少了需要 root 常驻的程序。你把一个服务从“root 运行”改成“普通用户 capability 运行”即使进程被攻破攻击者能拿到的系统权限也被压到只剩那几项能力。2.3 从 DAC 弱配置到提权几个常见坑DAC 弱配置是系统被渗透最常见的原因。我列一下运维阶段最容易留下隐患的几处根目录或/etc下出现 777 目录且属组为 root。任何低权限用户都可以读敏感配置、放恶意脚本。/tmp、/var/tmp没有粘滞位。攻击者拿到一个普通用户权限后可以在共享目录里放同名文件等 root 的 cron 执行。属主不匹配的 setuid 文件或者带写权限的 setuid 二进制。二进制 setuid 加上可写相当于任何人都能改写“root 会执行的内容”。硬链接/软链接配合可写目录诱导高权限程序写文件到非预期路径。检查时我会先跑一遍这三条把输出留档find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -ls find / -xdev -type d -perm -0002 -ls find / -xdev -type f -perm -0002 -ls世界可写或组可写的执行文件对生产环境都是风险信号。每次做基线检查这三条输出我都会和上次对比新增项必须有人解释清楚。DAC 弱点的真正危险在于它不产生日志改权限和删文件都是“合法”操作SELinux 至少还会留下一条 avc 审计记录DAC 出问题只能靠检查发现没有事后的后悔药。3. SELinux 强制访问控制规则、上下文与日常排错3.1 三种模式enforcing、permissive、disabledSELinux 当前生效状态用getenforce看运行中切换用setenforce 0|1。三者的区别是enforcing 严格执行策略违规访问直接拒绝并审计permissive 只记录违规不实际拦截适合上线前的策略验证disabled 则完全关闭内核安全模块。很多初学者喜欢直接搜“怎么关 SELinux”搜索引擎第一屏答案多半是setenforce 0但这是最差的一种解法。我强烈不建议在生产环境通过setenforce 0或者改/etc/selinux/config直接关闭。这样会让整套 MAC 失去作用而且如果系统里本来就存在 DAC 层面的弱配置等于把最后一道防线也拆了。合理的做法是用 permissive 模式跑业务收集审计日志核对哪些拒绝是误报再逐步收敛规则。注意setenforce只是临时生效重启会回到配置文件。真正关闭需要改配置文件后重启而且禁用后文件系统上的安全上下文不会自动消失重新启用时需要做一套恢复标签的动作反而更麻烦。3.2 安全上下文与类型强制SELinux 的核心语言SELinux 的检查单位叫安全上下文格式为user:role:type:mls_level例如system_u:system_r:httpd_t:s0。user 是 SELinux 用户不是 Linux 用户role 用于角色切换type 是类型强制里最重要的字段最后一个表示多级安全级别。拿一条实际命令来看ls -Z ps -Z id -Z系统里任何一个文件、进程、设备、socket 都有标签。进程的 type 通常称为“域domain”例如 httpd_t 就是 httpd 进程的域。一条典型的 allow 规则决定哪些域能对哪些类型做哪些操作allow httpd_t etc_t:file { read getattr open }类型强制的精妙之处在于虽然 httpd 是以 root 身份运行的但只要策略里没允许 httpd_t 写 /etc 下的文件它一样写不进去。这解决了传统 DAC 里“root 无所不能”的难题。内核每次做 open、exec、bind 等敏感操作时都会拿着当前进程的 scontext 和目标对象的 tcontext、tclass 去查策略库只有命中 allow 规则才会放行否则直接返回 EACCES 并写审计。3.3 标签修复、文件上下文映射与布尔值碰到程序因为文件标签不对被拒第一步是看标签ls -Z /path/to/file。如果只是个别人为改错了可以用chcon临时恢复但要永久生效必须让 SELinux 知道“这个路径下的文件默认该是什么类型”否则下次 restorecon 又会弹回去。标准做法是把路径映射写进文件上下文规则semanage fcontext -a -t httpd_sys_content_t /srv/www(/.*)? restorecon -Rv /srv/www很多发行版提供的audit2why和audit2allow极其好用。audit2allow -a会根据审计日志生成额外的 allow 规则但我通常不建议直接照单全收而应该逐条分析有些拒绝来自标签错位restorecon 改标签比加 allow 更合适有些拒绝来自进程域跑歪了需要修启动配置只有确实需要扩展权限的时候才允许策略顶进局部模块。盲目使用 audit2allow最后策略文件会膨胀到没法 review安全边界也名存实亡。布尔值是 SELinux 预留给管理员的开关例如 HTTP 服务能读用户家目录、能访问 NFS 等场景不用自己写规则直接用getsebool -a setsebool -P httpd_enable_homedirs on-P表示持久化。我见过很多人在没有检查布尔值的情况下加了一堆自定义规则最后重启丢配置还不如先查一遍布尔值省事得多。判断“该改布尔值还是该写 allow”的经验是如果这个开关在getsebool -a里已经存在先试布尔值如果布尔值里没有对应项再考虑自定义规则。3.4 一个真实场景Nginx 读文件被拒的排查流程某次部署我把源码放在/opt/appNginx 返回 403。检查ls -l看到rwxr-x---nginx 用户属于同名组DAC 层面完全没问题。但ls -Z一看出问题了目录类型是var_t不是标准的httpd_sys_content_tSELinux 认为 Nginx 域访问了一个不属于它的文件。当时按这个顺序处理getenforce确认确实在 enforcing。ausearch -m avc -ts recent查看最近的拒绝记录。audit2why -a给出人话解释提示标签不符。用semanage fcontext把/opt/app映射成httpd_sys_content_t再restorecon -Rv。刷新页面验证再看审计日志确认没有新的拒绝。这套顺序同样适用于 systemd 管理、Java 进程、数据库等几乎所有服务。排错原则是先分清到底是“类型不对”还是“权限不够”再决定是改标签还是改策略绝不能一上来就setenforce 0。另外提醒一句不要把服务配置文件放在/tmp下SELinux 对/tmp的默认标签很严格稍微跨域就会被拒把业务数据放在标准路径下会少很多麻烦。4. SEAndroidAndroid 里的 MAC 实践4.1 从 SELinux 到 SEAndroidSEAndroid 说白了就是 SELinux 在 Android 上的移植和定制。Android 内核依然走 LSM 框架但对移动端场景做了大量调整策略面向应用沙箱、系统服务、属性服务做裁剪工具链和策略文件不再使用完整的semanage体系而是改走 AOSP 的 sepolicy 构建流程。它的目标不是保护服务器上的 Apache 或 MySQL而是保护用户的隐私数据、系统服务的完整性以及应用之间的隔离。Android 从 4.3 引入 SELinux 时还是 permissive到 5.0 之后全面转为 enforcing。这一步的改变很关键因为应用生态里有大量通过/system写入、/data全局访问的粗放代码强制模式启用后权限收紧立刻暴露出一批乱用 uid、乱读他人数据的应用。今天用户拿到一台新手机每个应用默认被分配一个独立的、受限的 SELinux 域这就是 SEAndroid 日常在背后工作。它不要求开发者写规则而是系统发布时预置好一套完整策略。4.2 应用沙箱与域的划分Android 上每个普通应用基本对应untrusted_app或类似域系统应用、平台签名的应用则分别落到platform_app、system_app等域。Zygote 在 fork 应用进程后会根据应用的 uid、签名和安装信息设置进程的安全上下文。查看当前进程域adb shell ps -Z输出里像u:r:untrusted_app:s0:c512,c768这样的标签最后的 c 系列是 category用于进一步把不同应用隔离。文件侧的标签由file_contexts定义比如/data/user/0/pkg下的文件会标签成app_data_file并和应用的 uid/category 关联。这样即使两个应用都有可被利用的漏洞SELinux 域和文件 category 也会限制它们互相读数据。如果你在 AOSP 源码里翻过 sepolicy 目录会看到大量以app_开头的 type以及neverallow定义。这些规则很大程度上就是在做“应用不能碰系统资源”“普通应用不能碰其他应用数据”的边界。过去 Android 应用想越权读另一个应用的数据只要 DAC 层 uid 相同就行SEAndroid 启用后这条路基本堵死。4.3 改 sepolicy 的完整流程从 domain 到 allow做系统定制时最常碰到的需求是新增一个系统进程或者放开某个新硬件服务的访问。常规步骤是在 AOSP sepolicy 目录下新建.te文件声明一个新域例如my_daemon。在file_contexts中给相关文件/目录打标签例如把/vendor/bin/my_daemon标成my_daemon_exec_t。定义域转换在 init 的 service 配置里通过seclabel指定进程上下文通常还需要一个type_transition规则让执行入口文件触发域切换。编写 allow 规则给my_daemon域授权访问它需要的资源例如设备节点、socket、属性。用产品目录里的BOARD_SEPOLICY_DIRS或 AOSP 新版的BOARD_VENDOR_SEPOLICY_DIRS把附加策略纳入构建。编完刷机后先用adb shell setenforce 0或 permissive 模式验证功能再切回 enforcing 看 AVC 日志收尾。最需要警惕的是 neverallow 规则。AOSP 为了限制关键资源会写死一批禁止授权例如普通应用不能直接访问内核日志、不能读写其他应用的私有数据。即使你的 allow 规则在语义上看似合理构建时如果撞上 neverallow 仍会直接失败这不是“改一行 allow”能绕过的必须重新理解业务边界或者确认规则标签归属后调整。很多时候我发现开发者绕了一晚上最后发现其实只需要给文件打对标签根本不需要动 allow。4.4 AVC 日志解读与调试手段SEAndroid 的拒绝日志一般出现在内核日志或事件缓冲区特征是avc: denied开头。典型avc: denied { read } for pid1234 commmy_daemon nameconfig.cfg devmmcblk0p25 ino998 scontextu:r:my_daemon:s0 tcontextu:object_r:my_data_file:s0 tclassfile permissive0把这一段拆开denied { read }是被拒的操作scontext是发起者进程域tcontext是目标文件标签tclass是客体类别permissive0说明当前处于 enforce 状态这个拒绝是真实拦截而不是观察。调试时我习惯按“看日志 - 判断标签是否错 - 判断策略是否缺 - 小步授权 - 验证”的顺序走。dmesg | grep avc或logcat -b events | grep avc只是获取样本真正的修法要在 sepolicy 源文件里改。不要用audit2allow在用户态乱灌模块和 SELinux 一样大多数问题本质是标签打错了。Android 开发机上用 userdebug 版本可以方便地adb root、adb shell setenforce 0但生产固件里不要保留这种便利否则 MAC 形同虚设。5. DAC 与 MAC 共存这套权限体系怎么协同工作5.1 两次检查的执行顺序一个进程去 open 一个文件时内核走的是 VFS 层权限检查先做 DAC 校验再通过 LSM 钩子调 MAC 策略。因此DAC 不通过就不会到 MACDAC 通过了MAC 还能再否决一次。这就是为什么有时chmod 777之后仍然报 Permission denied——文件层面放行了SELinux 的域规则却把你按住了。DAC 先查是为了保持传统的 Unix 语义让旧的权限模型继续生效。MAC 后查则相当于在系统资源访问路径上加装总闸。这个顺序还带来一个重要结论排错时先确认 DAC再查 MAC尽量不要跳过任何一层。我见过不少案例开发人员把错误归因于“SELinux 抽风”结果用chmod 777全量放开之后真正的 DAC 权限错位反而暴露出来。安全设计上这两层各有缺陷靠叠加才构成完整防线。5.2 定位“谁挡了你”的命令组合综合排查我会先跑一套固定命令把现场信息收集齐id # 当前 Linux 用户和组 getenforce # SELinux 模式 ls -lZ # 文件的权限位和 SELinux 标签 ps -Z # 进程的域 ausearch -m avc -ts recent一个判断技巧如果ls -l显示文件可读程序仍然被拒再看ls -Z的标签和进程域是否匹配。比如 nginx 域是httpd_t目标文件是httpd_sys_content_t那就应该能读变成var_t多半就是文件上下文映射的问题不是 allow 规则缺失。先看标签再谈规则能让定位时间缩短一半。在容器和虚拟机环境里还要注意部分文件系统如虚拟机共享目录、NFS默认不支持扩展属性SELinux 标签可能查不到或者显示为默认值。Docker 场景里容器引擎通常会跟 LSM 配合给容器分配svirt_t/svirt_lxc_t这类域配合 cgroup 隔离提高边界。如果限制太严可以评估布尔值比如virt_use_nfs而不是直接关闭 SELinux。我见过有人把整个 Docker 数据目录chcon -Rt svirt_image_t结果镜像和容器共用标签反而让容器能读彼此镜像边界瞬间崩塌。5.3 两层权限同时生效时的经典案例举一个我实际踩过的例子一台机器上以 systemd 运行自定义服务代码包里有一个.so动态库ls -l是 755属主完全正确。服务启动时一直报段错误后来看 AVC 日志才发现动态库被 restorecon 标成了lib_t但服务域没有读取lib_t的权限。一面看 DAC 完全正常另一面 MAC 根本不放行光靠“文件是 755 啊”是看不出问题的。最后把动态库文件上下文改成服务预期的类型并 restorecon问题消失。这个案例说明DAC 和 MAC 不是“任一放行即可”而是“层层都要通过”。权限问题定位时养成同时查看传统权限位和 SELinux 标签的习惯能省掉大量试错。尤其是服务迁移和备份恢复场景tar 解包时如果没带 SELinux 属性标签会全部变成unconfined_u:object_r:default_t这种隐性变化不主动查根本发现不了。5.4 应用这套思路的几条工程经验最后整理几条长期踩出来的经验部署脚本里加restorecon -Rv或semanage fcontext记录尽量不在生产环境用chcon改标签。新增服务或应用前先在 permissive 下观察一段时间用审计日志补齐策略再切换 enforcing。能用布尔值和 SRE 手段解决的就不要写自定义 allow 规则规则越少越不容易撞上 neverallow。日志中的comm和tclass是一对非常有用的线索前者告诉你谁发起的后者告诉你是文件、目录、socket 还是其他客体类别。所有允许规则都过一遍变更评审因为 MAC 策略一旦放开收回比放开难得多。6. 我的实际操作习惯每接一台新服务器或者新刷一台开发板我会先把权限模型的状态写进基础信息getenforce、SELinux 的配置文件、关键服务的上下文清单。不只是为了排查方便更是让团队知道我们部署的系统是在哪一层安全模型下运行的。真到线上出问题那天没有人愿意临时翻文档猜“这台机器是不是开了 SELinux”。Android 定制项目里我个人的习惯是把 sepolicy 变更跟功能模块绑定在一起而不是把所有域名混在同一个全局策略文件。这样哪个进程访问了什么、为什么需要放开都能在代码 review 时被看到。权限模型的设计从来不是“越开放越好”也不是“越严格越好”而是在给定威胁环境下找到够用的边界。如果你正在配置服务时被avc: denied挡住建议先多花十分钟看看审计日志把域、标签、类别理清楚再动手。很多时候这一层被拒恰恰说明 MAC 在正常工作真正该做的是让规则适配业务而不是把安全模块关掉了事。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →