起因
- 咱的 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.info
和 org.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。