SafeSetID

SafeSetID 是一个 LSM 模块,它限制 setid 系列系统调用,以将 UID/GID 转换从给定的 UID/GID 限制为仅系统范围的允许列表批准的那些。这些限制还禁止给定的 UID/GID 获取与 CAP_SET{U/G}ID 关联的辅助权限,例如允许用户设置用户命名空间 UID/GID 映射。

背景

在没有文件 capabilities 的情况下,需要在 Linux 系统上切换到不同用户的进程必须以 CAP_SETUID 权限启动。 CAP_SETUID 授予以 root 身份运行的程序或显式授予 CAP_SETUID 运行时 capability 的非 root 用户运行的程序。 通常最好使用 Linux 运行时 capabilities 而不是文件 capabilities,因为使用文件 capabilities 以提升的权限运行程序会打开可能的安全漏洞,因为任何有权访问该文件的用户都可以 exec() 该程序以获得提升的权限。

虽然可以通过赋予完整的 CAP_SET{U/G}ID capabilities 来实现进程树,但这通常与在非 root 用户下运行进程树的目标相悖。 具体来说,由于 CAP_SETUID 允许更改为系统上的任何用户,包括 root 用户,因此对于这种情况来说,它是一种过于强大的 capability,特别是当程序通常只调用 setuid() 来降低到权限较低的用户时,而不是提升权限。 不幸的是,在 Linux 中,除了允许切换到系统上的任何用户之外,没有普遍可行的方法来限制用户可以通过 setuid() 切换到的潜在 UID。 此 SafeSetID LSM 旨在提供一种以这种方式限制 setid capabilities 的解决方案。

此 LSM 的主要用例是允许非 root 程序在没有完全 CAP_SETUID capabilities 的情况下转换为其他不受信任的 uid。 非 root 程序仍然需要 CAP_SETUID 才能进行任何类型的转换,但是此 LSM 施加的额外限制意味着它是 CAP_SETUID 的“更安全”版本,因为非 root 程序无法利用 CAP_SETUID 进行任何未经批准的操作(例如,setuid 到 uid 0 或创建/进入新的用户命名空间)。 更高级的目标是允许基于 uid 的系统服务沙箱,而不必到处都赋予 CAP_SETUID,仅仅是为了让非 root 程序可以降低到权限更低的 uid。 当系统上的一个非 root 守护进程应该被允许以不同的 uid 生成其他进程时,这一点尤其重要,但不希望赋予该守护进程基本上等同于 root 的 CAP_SETUID。

考虑的其他方法

在用户空间解决此问题

对于希望具有此 LSM 中实现的受限 setid capabilities 的候选应用程序,另一种选择是完全从应用程序中删除 setid capabilities,并重构应用程序中的进程生成语义(例如,使用特权 helper 程序来执行进程生成和 UID/GID 转换)。 不幸的是,围绕进程生成存在许多语义,这些语义会受到影响,例如 fork() 调用,其中程序在 fork() 之后不会立即调用 exec(),父进程为生成的子进程指定自定义环境变量或命令行参数,或者文件句柄在 fork()/exec() 之间继承。 因此,使用用户空间中的特权 helper 的解决方案可能不太吸引人,无法合并到依赖于 Linux 中某些进程生成语义的现有项目中。

使用用户命名空间

另一种可能的方法是在其自己的用户命名空间中运行给定的进程树,并赋予树中的程序 setid capabilities。 这样,树中的程序可以在其自身的用户命名空间的上下文中更改为任何所需的 UID/GID,并且只有经过批准的 UID/GID 可以映射回初始系统用户命名空间,从而有效地防止了权限提升。 不幸的是,通常无法单独使用用户命名空间,而不将其与其他命名空间类型配对,这不是总是可行的。 Linux 基于“拥有”某些实体的用户命名空间检查 capabilities。 例如,Linux 有网络命名空间由创建它们的用户的用户命名空间拥有的概念。 由此产生的结果是,检查对给定网络命名空间的访问的 capability 是通过检查任务在拥有网络命名空间的用户命名空间的上下文中是否具有给定的 capability 来完成的,而不一定是给定任务在其下运行的用户命名空间。 因此,在新用户命名空间中生成进程实际上会阻止其访问由初始命名空间拥有的网络命名空间。 对于任何期望保留 CAP_NET_ADMIN capability 以调整网络配置的应用程序来说,这是一个决定性因素。 单独使用用户命名空间会导致与其他系统交互相关的问题,包括 pid 命名空间和设备创建的使用。

使用现有的 LSM

其他任何 in-tree LSM 都不具备限制 setid 转换的 capability,甚至根本不使用 security_task_fix_setuid hook。 SELinux 对该 hook 说:“由于 setuid 只影响当前进程,并且由于 SELinux 控制不基于 Linux 身份属性,因此 SELinux 不需要控制此操作。”

使用说明

此 LSM hook setid 系统调用以确保如果存在适用的限制策略,则允许转换。 策略通过 securityfs 进行配置,方法是将内容写入 securityfs 安装位置的 safesetid/uid_allowlist_policy 和 safesetid/gid_allowlist_policy 文件。 添加策略的格式为“<UID>:<UID>”或“<GID>:<GID>”,使用字面数字,并以换行符结尾,例如“123:456n”。 写入空字符串“”将刷新策略。 同样,为 UID/GID 配置策略将阻止该 UID/GID 获得辅助 setid 权限,例如允许用户设置用户命名空间 UID/GID 映射。

关于 GID 策略和 setgroups() 的说明

在 v5.9 中,我们正在添加对限制 CAP_SETGID 权限的支持,就像之前对 CAP_SETUID 所做的那样。 但是,为了与用户空间中常见的沙箱相关代码约定兼容,我们目前允许对具有 CAP_SETGID 限制的进程进行任意 setgroups() 调用。 在我们在未来的版本中添加对限制 setgroups() 调用的支持之前,这些 GID 策略没有增加任何有意义的安全性。 一旦我们有了策略检查代码,setgroups() 限制将被强制执行,这将依赖于 v5.9 中添加的 GID 策略配置代码。