起因

  • 咱的 vps 上有一个时不时需要 root 权限执行特定命令的服务,但是咱又不想让这个服务以 root 权限运行。

Polkit 介绍

  • Polkit 是一套应用程序级别(application-level)的工具集,用来定义规则以及授权进程以其它用户的权限运行命令,分为操作(Actions)和认证规则(Authorization rules)两个部分。
  • 每个操作的政策由安装的软件包来设定,包含在一个 XML 格式的政策文件中,比如 /usr/share/polkit-1/actions/org.fedoraproject.FirewallD1.policy 就定义了 Firewalld 的默认政策(例如普通用户是否可以查询防火墙信息或者修改规则等)。注意对政策文件的修改可能会在包升级时被恢复或者覆盖,所以需要通过修改认证规则来达到自定义的效果。
  • 认证规则则放置在 /usr/share/polkit-1/rules.d 或者 /etc/polkit-1/rules.d,后缀名为 .rules,并使用 JavaScript 语法定义。
    这也是我们需要设置的地方也就是本文的重点。
  • 与 sudo 不同,polkit 并非赋予一个进程完整的 root 权限,而是通过一个策略系统(操作 + 认证规则)来进行更加精细的授权。
    比如当你运行 sudo hostnamectl set-hostname baka 时,系统会询问你密码,然后使用完整的 root 权限运行该命令。
    然而假如 hostnamectl 配置了相应的 polkit 政策,用户就可以直接运行 hostnamectl set-hostname baka,然后进程会读取相应的政策文件,并按照进程中被访问的操作来进行相应的处理,例如询问密码、直接拒绝,或者按照认证规则(Authorization rules)里的政策来根据用户名和用户组等信息进行判断。
  • 这里需要特别提一下 pkexec,它是 pokit 中包含的一条命令,与 sudo 一样,其作用也是以其它用户的权限执行命令,例如 pkexec --user root whoami。所以实际上,与 sudo 进行比较的应该是 pkexec 而不是 pokit,而 pokit 与 sudo 最主要的区别在于 pkexec 是通过 polkit 框架来实现的,也就是说可以通过配置与 pkexec 相关的认证规则来控制其行为。

例子 -0 读懂操作政策

政策文件存放在 /usr/share/polkit-1/actions/,一般以 "org.组织.项目.policy" 的形式命名,格式为 XML,例如 Firewalld 的政策文件存放在 /usr/share/polkit-1/actions/org.fedoraproject.FirewallD1.policy,内容大概是这样子的:

<vendor>FirewallD</vendor>
<vendor_url>http://firewalld.org</vendor_url>
...
<action id="org.fedoraproject.FirewallD1.info">
  <description>General firewall information</description>
  <message>System policy prevents getting general firewall information</message>
  <defaults>
    <allow_any>yes</allow_any>
    <allow_inactive>yes</allow_inactive>
    <allow_active>yes</allow_active>
  </defaults>
</action>
...

其中每一个 action tag 都定义了一个『操作』,比如示例中的操作为 org.fedoraproject.FirewallD1.info,而 <allow_xxx>xxx</allow_xxx> 分别表示了该操作在远程会话(alow_inactive)、本地会话(allow_active)以及前两者(allow_any)中都是可以不进行认证(yes)就执行的,这三种设置的值可以为:

  • no:不允许任何用户执行操作。
  • yes:用户可以不进行认证就执行操作。
  • auth_self:用户只需输入自己的密码即可通过认证并执行操作。
  • auth_admin:需要用户认证为管理员才能通过认证并执行操作。
  • auth_self_keep:和 auth_self 类似,认证状态会保持一段时间。
  • auth_admin_keep:和 auth_admin 类似,认证状态会保持一段时间。

例子 +0 对认证规则的调试/输出

假如我们希望记录请求信息,新建 /etc/polkit-1/rules.d/00-log-access.rules:

polkit.addRule(function(action, subject) {
  polkit.log("action=" + action);
  polkit.log("subject=" + subject);
});

当用户 apiserver 试图读取 Firewalld 规则时提示访问被拒绝:

$ firewall-cmd --list-all
Authorization failed.
Make sure polkit agent is running or run the application as superuser.

然后使用 journalctl -u polkit | tail -n 100 来查看日志,可以看到:

Jan 01 00:00:00 apiserver polkitd[24467]: /etc/polkit-1/rules.d/00-log-access.rules:2: action=[Action id='org.fedoraproject.FirewallD1.info']
Jan 01 00:00:00 apiserver polkitd[24467]: /etc/polkit-1/rules.d/00-log-access.rules:3: subject=[Subject pid=25855 user='apiserver' groups=apiserver seat=null session='166' local=false active=true]
Jan 01 00:00:00 apiserver polkitd[24467]: /etc/polkit-1/rules.d/00-log-access.rules:2: action=[Action id='org.fedoraproject.FirewallD1.config.info']
Jan 01 00:00:00 apiserver polkitd[24467]: /etc/polkit-1/rules.d/00-log-access.rules:3: subject=[Subject pid=25855 user='apiserver' groups=apiserver seat=null session='166' local=false active=true]

可以看到用户 apiserver 试图访问了 org.fedoraproject.FirewallD1.infoorg.fedoraproject.FirewallD1.config.info 这两个操作以及相关的信息。

例子 1 对特定程序设置规则

允许用户 apiserver 执行 firewall-cmd --list-all 也就是查看防火墙配置,新建 /etc/polkit-1/rules.d/30-firewalld-apiserver.rules:

polkit.addRule(function(action, subject) {
  if (action.id == "org.fedoraproject.FirewallD1.config.info") &&
      (subject.user == "apiserver") {
    return polkit.Result.YES;
  }
});

顺便一提这里并不需要考虑 action.id == "org.fedoraproject.FirewallD1.info" 的情况,因为在例子 -0 中已经了解到 org.fedoraproject.FirewallD1.info 操作默认是不需要认证的。

或者允许 apiserver 对 Firewalld 的全部访问(包括修改等):

polkit.addRule(function(action, subject) {
  if (action.id == "org.fedoraproject.FirewallD1.all" &&
      subject.user == "apiserver") {
    return polkit.Result.YES;
  }
});

注意此时用户 apiserver 拥有了操作 Firewalld 的全部权限,也就是说所有 firewall-cmd xxxxx 的命令都会被允许运行,下面一个例子将会讲述如何对此进行限制。

例子 2 对特定命令设置规则

实际上我希望仅授权 apiserver 运行符合特定格式的命令,例如:

firewall-cmd --add-rich-rule "rule family=ipv4 source address=xxx.xxx.xxx.xxx port port=22 protocol=tcp accept"

但通过查看 Firewalld 的操作文件(例子 -0)和 polkit 的日志(例子 +0)发现,我们所获得的信息并不包含完整的命令,所以需要一个方法来解决这个问题,我选择的是通过 pkexec 来执行 firewall-cmd

pkexec /usr/bin/firewall-cmd --add-rich-rule "rule family=ipv4 source address=123.45.67.89 port port=22 protocol=tcp accept"

pkexec 会提供更加详细的信息:

Jan 01 00:00:00 apiserver polkitd[24467]: /etc/polkit-1/rules.d/00-log-access.rules:2: action=[Action id='org.freedesktop.policykit.exec' program='/usr/bin/firewall-cmd' user.display='root (root)' command_line='/usr/bin/firewall-cmd --timeout=259200 --add-rich-rule "rule family=ipv4 source address=206.253.138.34 port port=22777 protocol=tcp accept"' user='root' polkit.message='Authentication is needed to run `$(program)' as the super user' polkit.gettext_domain='polkit-1' user.gecos='root']
Jan 01 00:00:00 apiserver polkitd[24467]: /etc/polkit-1/rules.d/00-log-access.rules:3: subject=[Subject pid=25967 user='apiserver' groups=apiserver seat=null session='166' local=false active=true]

我们这时需要做的事情也很简单了,通过配置 pkexec 的认证规则,来对 pkexec 所调用的命令进行一次正则表达式的判断,然后在判断通过的情况下允许 apiserver 以 root 的权限运行该命令:

polkit.addRule(function(action, subject) {
  if (action.id == "org.freedesktop.policykit.exec" &&
    action.lookup("program") == "/usr/bin/firewall-cmd" && 
    action.lookup("user") == "root" &&
    subject.user == "apiserver") {
      var command = action.lookup("command_line");
      var re = /\/usr\/bin\/firewall-cmd --add-rich-rule "rule family=ipv4 source address=(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]) port port=22 protocol=tcp accept"/;
      if (re.test(command)) {
        return polkit.Result.YES;
      } else {
        return polkit.Result.NO;
      }
  }
});

例子 3 添加更多判定条件

从用户名 apiserver 可以看出来,这是一个专门用来运行服务的用户,所以我们自然希望进行更严格的限制(例如只能在没有 session 的情况下运行):

polkit.addRule(function(action, subject) {
  if (action.id == "org.freedesktop.policykit.exec" &&
    action.lookup("program") == "/usr/bin/firewall-cmd" &&
    action.lookup("user") == "root" &&
    subject.user == "apiserver" &&
    subject.session == null) {
      var command = action.lookup("command_line");
      var re = /\/usr\/bin\/firewall-cmd --add-rich-rule "rule family=ipv4 source address=(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]) port port=22777 protocol=tcp accept"/;
      if (re.test(command)) {
        return polkit.Result.YES;
      } else {
        return polkit.Result.NO;
      }
  }
});

在添加了 subject.session == null 这一条件之后,如果我们试图直接在终端中 su apiserver 然后再运行前面的命令时,就会被拒绝。
另外 subject 变量中各个属性所代表的含义如下:

  • pid:进程的PID。
  • user:运行进程的用户。
  • groups:运行进程的用户所属的全部用户组。
  • seat:该进程所属的席位(seat),当没有席位时为 null。
  • session:该进程所属的会话(session),当没有 session 时为空值。
  • local:当进程属于一个本地 seat 的时候为 true。
  • active:当进程属于一个本地会话的时候为 true。

总结

似乎也没有什么需要总结的,总而言之,遇到脚本中需要高权限的情况时,不妨考虑一下使用 polkit 和 pkexec。