polkit 与 pkexec 权限委派实战:细粒度授权模型、.policy 规则与 PwnKit 提权加固全流程

为什么有了 sudo,还需要 polkit

几乎每个 Linux 服务器管理员都熟悉 sudo:把某个用户加进 sudoers,或者用 NOPASSWD 放行某条命令,就能临时获得 root 权限。但如果你管理过带图形界面的桌面系统、或者给非 root 的守护进程做过权限委派,就会遇到一个 sudo 解决不了的问题:sudo 的授权模型是"以某个用户的身份执行命令",它默认整条命令都要以 root 跑,而且它假设调用者坐在一个交互式终端前。

真正的问题是:当一个普通用户的服务进程需要做一件特权操作(比如挂载一个 U 盘、修改系统时间、重启 NetworkManager)时,我们并不想给他完整的 root,也不想把密码塞进脚本里。这正是 polkit(PolicyKit)存在的原因——它是一套"细粒度的权限委派框架",让非特权进程可以通过一个受控的 D-Bus 接口,请求执行特定的特权操作,而由系统管理员预先写好策略决定"谁能做、要不要输密码、在什么条件下可以做"。

本文会从概念讲到落地:polkit 的授权模型、.policy 规则文件怎么写、pkexec 与 sudo 的本质区别、如何用 pkaction / pkcheck 调试,以及最关键的——2021 年轰动一时的 PwnKit(CVE-2021-4034)漏洞是怎么被 pkexec 的一个参数处理疏忽引爆的,以及我们应该如何加固。

polkit 与 sudo 的本质区别:一个是"执行命令",一个是"授权动作"

先把两者摆在一起看,避免把 polkit 当成 sudo 的替代品:

  • sudo:授权单位是"命令"。你在 sudoers 里写 alice ALL=(root) /usr/bin/apt update,意思是 alice 可以以 root 身份跑这条命令。sudo 会 fork 一个 root 进程,命令的 uid 真的变成了 0。授权发生在"进程启动"这一刻。
  • polkit:授权单位是"动作(action)"。每个动作有一个全局唯一的 ID,比如 org.freedesktop.udisks2.filesystem-mount。系统里某个进程(客户端)通过 D-Bus 调用特权服务(机制方 mechanism),特权服务在处理前会调用 polkit 询问"这个客户端有没有资格做这个动作"。授权发生在"动作执行"这一刻,而调用者可能只是个普通用户程序。

换句话说,sudo 是"我以 root 的身份去跑一个东西",polkit 是"我请一个本来就以 root 运行的守护进程,替我做一件被授权的事"。桌面上的磁盘挂载、网络配置、时间设置、软件安装对话框,背后几乎都是 polkit 在工作。对服务器场景来说,polkit 的价值在于:你可以让一个长期运行的非 root 服务(监听 80 端口的自研程序、需要调整防火墙规则的 agent)在需要时申请到恰好那一个动作的权限,而不是整条命令的 root,也不会把 root 密码写进配置。

polkit 的授权模型:主体、动作、代理与规则

理解 polkit 需要先理清四个角色:

  • Subject(主体):发起请求的一方。通常是一个进程,polkit 会追溯到它的用户、所属会话(session)、是否在本地、是否活动的登录会话。注意"会话"很重要——很多规则只在"本地活跃会话"里才允许交互式授权。
  • Object / Action(动作):被请求的特权操作,由 .policy 文件向 polkit 注册,有唯一 action id。
  • Authority(授权方):就是 polkit 守护进程(polkitd)本身,它读取规则并作出"允许/拒绝/需要认证"的裁决。
  • Mechanism(机制方):真正执行特权操作的守护进程(如 udisks2、systemd-logind、NetworkManager)。它是裁决的执行者,也是向 polkit 提问的人。

授权的判定优先级是自上而下的:

  1. 先看 .policy 里定义的默认值(allow_any、allow_inactive、allow_active)。
  2. 再看管理员写的 rules(JavaScript 规则文件),它可以覆盖默认值,按用户、组、session 类型做更细的判定。
  3. 如果没有直接放行,就按动作定义的策略要求认证(可能是管理员密码,也可能是调用者自己的密码)。

这里有个容易踩的点:polkit 的规则是"只增不减"的叠加逻辑。一条规则可以显式返回 polkit.Result.YES 放行,也可以返回 polkit.Result.NO 拒绝,但如果没有任何规则命中,才会回落到 .policy 的默认值。很多人写了规则却发现不生效,原因往往是 action id 拼错、或者没放在正确的规则目录里。

动手:用一个自定义动作把权限授给非 root 用户

假设我们有一个自研的运维脚本 /usr/local/bin/restart-web.sh,它需要以 root 运行。我们不想把用户加进 sudoers,而是定义一个 polkit 动作,允许特定组的用户在本地会话里授权执行。分两步:先定义动作,再写规则。

第一步,写动作定义文件 /usr/share/polkit-1/actions/com.example.restart.policy:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
 "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
  <vendor>Example Inc.</vendor>
  <action id="com.example.restart-web">
    <description>Restart the example web service</description>
    <message>Authentication is required to restart the web service</message>
    <defaults>
      <allow_any>no</allow_any>
      <allow_inactive>no</allow_inactive>
      <allow_active>auth_admin_keep</allow_active>
    </defaults>
    <annotate key="org.freedesktop.policykit.exec.path">/usr/local/bin/restart-web.sh</annotate>
    <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
  </action>
</policyconfig>

三个默认值的含义要记牢:allow_any 管"任意客户端",allow_inactive 管"非活跃会话(比如 SSH 远程、后台)",allow_active 管"本机活跃登录会话"。取值可以是 yes(直接放行)、no(直接拒绝)、auth_admin(需要管理员认证一次)、auth_admin_keep(认证后短时间内记住)、auth_self / auth_self_keep(需要调用者本人认证)。

第二步,写规则文件 /etc/polkit-1/rules.d/49-example-restart.rules:

polkit.addRule(function(action, subject) {
    if (action.id == "com.example.restart-web" &&
        subject.isInGroup("webops") &&
        subject.local &&
        subject.active) {
        return polkit.Result.YES;
    }
});

这段规则的意思是:只有当请求动作是 com.example.restart-web、发起者在 webops 组、并且是本地活跃会话时,才无条件放行。注意 polkit 规则用的是 JavaScript(通过 polkit 引擎执行),文件必须放在 /etc/polkit-1/rules.d/ 下,权限不能是全局可写,否则 polkit 为了安全会直接忽略它。修改后不需要重启 polkitd,规则会在下一次请求时被重新加载。

然后用 pkexec 就可以把动作和实际命令绑定起来执行:

pkexec /usr/local/bin/restart-web.sh

# 查看系统里注册了哪些动作
pkaction --verbose | head -40

# 查看某个动作的当前默认策略
pkaction --action-id com.example.restart-web --verbose

# 以某个用户/会话为身份,测试是否被授权
pkcheck --action-id com.example.restart-web \
        --process $$ --allow-user-interaction

pkexec 和 sudo 到底差在哪:一个参数处理的疏忽引发的 PwnKit

pkexec 是 polkit 自带的命令行工具,它允许用户通过 polkit 的授权流程执行一条命令,默认以 root 身份跑——看起来和 sudo 很像,但机制完全不同:sudo 读 sudoers 决定授权,pkexec 查 polkit 动作决定授权。pkexec 在没有匹配到 org.freedesktop.policykit.exec 动作的规则时,会回落到"需要管理员认证"的默认行为。

真正的教训来自 2022 年初公开的 PwnKit(CVE-2021-4034)。这个漏洞在 pkexec 里潜伏了十二年:pkexec 处理命令行参数时,对 argc(参数个数)做了不够严谨的边界处理。当 pkexec 以"没有传入任何参数"的方式被调用时,它的内部数组处理和真实内存布局产生了偏差,导致环境变量数组 envp 和参数数组 argv 在内存中相邻的这段空间被错误使用。攻击者只要精心构造一个名为 pwnkit 之类的可执行文件,并通过 GCONV_PATH、PKEXEC_UID 等环境变量做重定向,就能让 pkexec 以 root 身份加载攻击者控制的代码,从而在任何打了补丁前的发行版上直接拿到 root。

这个漏洞的可怕之处在于:它不需要任何特殊的系统配置,只要机器上存在 SUID 位的 pkexec(几乎所有带 polkit 的发行版默认都有),任何本地普通用户都能一键提权,而且利用代码公开后极其稳定。修复的方式是升级 polkit 到已修补版本,或者在无法立即升级时,直接去掉 pkexec 的 SUID 位:

# 查看 pkexec 的权限位(注意 s 就是 SUID)
ls -l /usr/bin/pkexec

# 临时缓解:去掉 SUID 位(会影响依赖 pkexec 的图形/服务功能)
chmod 0755 /usr/bin/pkexec

# 正确做法:升级到官方修补版本
apt-get update && apt-get install --only-upgrade policykit-1

需要强调的是,去掉 SUID 只是应急手段,会让所有依赖 pkexec 的动作失效。生产上应该优先升级,然后用一句话验证:pkexec --version 确认版本,再对照发行版安全公告核对修补版本号。

一次真实复盘:规则不生效,日志里早就写明了原因

我曾在一个内部服务上遇到过这样的现象:为了让某个监控 agent 能重启服务,我按上面的写法加了动作和规则,pkaction 也能看到动作,但 agent 调用时始终失败,报 Not authorized。第一反应是规则写错了,于是反复改 JavaScript 判断条件,折腾了一个下午。

后来想起来 polkit 有日志,去 journalctl -u polkit 一看,真相立刻浮现:日志里明确写着 Operator of action ... is not authorized,并标注了 subject.local=false, subject.active=false。原来这个 agent 是通过一个非交互式、非本地会话触发的,属于 allow_inactive 管辖的范围,而我的规则里写了 subject.local && subject.active,直接把这次请求挡在了门外。把规则里的这两个条件去掉、改为允许 inactive 会话(同时用组来收口范围)之后,问题立刻解决。

这个坑的通用经验是:调试 polkit 的第一件事永远是看 journalctl -u polkit,它会清楚地告诉你动作 ID、请求主体、以及三个上下文标志(local/active/session)的真实取值。盲目改规则远不如先读日志。另外,规则文件里任何 JS 语法错误也会在日志里体现为 Error evaluating rules,这也是最常见的"规则明明写了却不生效"的原因之一。

polkit 场景下的安全加固清单

结合上面的机制,给出一份可以直接落地的加固清单:

  • 及时升级 polkit。PwnKit 之后还有若干提权类 CVE,保持 policykit-1 是最新版是底线,把它纳入常规安全更新。
  • 审慎对待 SUID 与 pkexec。若服务器完全用不到 polkit 的交互式授权(纯服务器环境很常见),评估去掉 pkexec 的 SUID 位,减少一个本地提权面。
  • 规则文件权限收口。/etc/polkit-1/rules.d/ 下的文件不能被非 root 写入,否则会变成一个"谁能改规则谁就能提权"的后门。检查 ls -l /etc/polkit-1/rules.d/。
  • 默认值宁紧勿松。自定义动作的 allow_any / allow_inactive 一律先设 no,确有需要再用规则按组放行,绝不用 yes 图省事。
  • 用组而不是单个用户名做规则。这样人员变动时只需调整组成员,规则本身保持稳定,也更容易审计。
  • 定期核对动作清单。pkaction 列出所有注册动作,留意有没有来源不明的第三方软件注册了高权限动作。

最后回到定位问题:polkit 不是 sudo 的替代品,而是 sudo 之外的第二种委派思路——把"执行某条命令的完整 root"降级为"请求某个受控动作"。对个人站长来说,服务器上大多用不到图形化的 polkit 场景,但理解它的授权模型、知道 pkexec 的 SUID 风险和 PwnKit 这类漏洞的来龙去脉,能让你在做安全加固和安全更新时,多一份对"权限从哪来、能到哪去"的清晰认知。把 pkaction、pkcheck、journalctl -u polkit 这三样工具用熟,排查 polkit 问题基本就成功了一大半。

Last modification:October 16th, 2026 at 07:38 pm

Leave a Comment