This is a cleaned up and more concrete version of my proposal about polkit support from a few years ago. The proposed design for the polkit
interface is mostly finished. There are some areas where polkit-agent
needs to be fleshed out a bit, but I think it is at a point where it could do with some feedback.
Here are one sentence summaries of what the two interfaces enable:
-
polkit
: allow daemons permission to use polkitd to make access control decisions for requests from unprivileged clients. -
polkit-agent
: allow a snap to act as a polkit agent to grant access to a group of related processes.
The first is the more generally useful interface. The second would mostly be useful on Ubuntu Core based systems that implement a user session.
polkit
interface
Description of access
To perform polkit authorization checks, a daemon needs to do two things:
- Install a .policy file to
/usr/share/polkit-1/actions
describing the actions it will use (codifying the type of administrative access a user might be granted). - Before performing administrative work on behalf of a client app, make a
CheckAuthorization
D-Bus call to polkitd to ask if they have access. The D-Bus call passes a string action ID describing the access, and a “subject” struct describing the client application.
There are two primary ways a daemon can describe the subject of the check:
- For D-Bus daemons they can use a
system-bus-name
subject, sending the unique bus name of the client app. - For non-D-Bus daemons, they can use a
unix-process
subject, sending the process ID (as retrieved throughSO_PEERCRED
orSCM_CREDENTIALS
).
In the initial implementation of the interface, I suggest we focus on daemons using system-bus-name
subjects, since they are more common and can be more strictly confined.
Proposed Implementation
I propose that a snap would define a plug with something like:
plugs:
polkit:
action-prefix: "org.example.foo"
The action-prefix
plug attribute would be mandatory, and indicate that all actions published by the snap are equal to the action prefix or match ${action-prefix}.*
.
An implicit polkit
slot would be provided on systems where polkit authorisation is available. At present this means it would be implicitOnClassic
. If we add polkit to Ubuntu Core in future, it
would make sense to provide the slot there as well.
The base declaration should probably look somewhat similar to e.g. system-files
, requiring a snap declaration from the store to install that would give some oversight to make sure the prefix was set
appropriately.
The connected plug AppArmor rules for the interface would be as follows:
# Allow communication with polkitd on the D-Bus system bus
#include <abstractions/dbus-strict>
dbus (send)
bus=system
path="/org/freedesktop/PolicyKit1/Authority"
interface="org.freedesktop.PolicyKit1.Authority"
member="{,Cancel}CheckAuthorization"
peer=(name="org.freedesktop.PolicyKit1", label=unconfined),
dbus (send)
bus=system
path="/org/freedesktop/PolicyKit1/Authority"
interface="org.freedesktop.DBus.Properties"
peer=(name="org.freedesktop.PolicyKit1", label=unconfined),
dbus (send)
bus=system
path="/org/freedesktop/PolicyKit1/Authority"
interface="org.freedesktop.DBus.Introspectable"
member="Introspect"
peer=(name="org.freedesktop.PolicyKit1", label=unconfined),
In addition, the interface code will expect a file $SNAP/meta/${plug_name}.policy
to exist. When the plug is connected, this file will be validated to make sure it conforms to the documented format and only describes actions matching the specified action-prefix
. There is an initial implementation of this validation in snapd PR #9986.
When the plug is connected, the .policy file will be copied to /usr/share/polkit-1/actions/snap.${SNAP_INSTANCE_NAME}.${plug_name}.policy
. When the plug is disconnected, the file will be removed. On snap removal, all snap.${SNAP_INSTANCE_NAME}.*.policy
files should be
removed from that directory.
Questions
Could the .policy files be auto-generated instead of being provided by the snap?
This was something previously suggested by @jdstrand, with e.g. the set of actions for NetworkManager being encoded into the network-manager
interface, and output when the corresponding interface is plugged. There are a number of issues that I think make this unworkable:
- the list of actions a particular daemon uses can change from version to version.
- the .policy file contains translations of action messages and descriptions. Even if the list of actions don’t change between two releases, it is not uncommon for the daemon to pick up new translations.
What about daemons not using D-Bus?
The main difficulty in using the unix-process
subject type is that you need to include start-time
of the process as a way to protect against pid reuse attacks. This information is not provided in the
struct ucred
, and needs to be read from /proc
.
So a daemon using this method will need read access to /proc/*/stat
. We can’t restrict it with @owner
either, since the whole point is to allow daemons running as root to act on behalf of unprivileged users.
We could handle this in one of two ways:
- require such daemons to plug
system-observe
, which grants the required access plus a lot more. - add another plug attribute to enable the required access.
Should snaps be able to install polkit rules?
I think we should punt on this in the short term. At present, there are two incompatible formats for rules:
- the
.pkla
ini-style rule files supported by polkit <= 0.105 - the
.rules
JavaScript files supported by polkit > 0.105.
At present, Debian-derived distros (including Ubuntu) ship polkit 0.105 with many patches from later versions applied.
It wouldn’t be too difficult to validate .pkla
files, but they will be ignored by most non-Debian distributions. In contrast, .rules
files are arbitrary JavaScript. While polkitd does run them in a web browser style sandbox and imposes a strict time limit, it does represent a way to execute code in the context of an unconfined system daemon.
So I think it best to not support this until we have a concrete use case.
polkit-agent
interface
Description of access
A polkit agent registers with polkitd as being responsible for authorising requests on behalf of other processes, usually those belonging to a particular logind session ID. The control flow essentially goes like this:
- agent registers itself with polkitd by calling its
RegisterAuthenticationAgentWithOptions
method. This provides an object path on which polkitd can send method calls to the agent. - When polkitd is asked to perform an authorization check for a process the agent is responsible for, the agent receives a
BeginAuthentication
method call. - The agent verifies that the user’s password with the help of the setuid
/usr/libexec/polkit-agent-helper-1
helper. - On completion,
polkit-agent-helper-1
sends calls polkitd’sAuthenticationAgentResponse2
method to report the status.
The polkit-agent-helper-1
process uses PAM to validate the password, so can not reasonably run on a classic system where there could be any PAM modules registered. We have a bit more certainty on Ubuntu Core systems where the account system is more regular.
Proposed implementation
On relevant Ubuntu Core systems, an implicit system slot would be defined for the polkit-agent
interface. Snaps that wish to act as polkit agents would have a corresponding plug. This interface would not require interface attributes.
The connected plug AppArmor rules for this interface would preferably transition to a sub-profile on execution of polikit-agent-helper-1
. I’m not yet sure exactly what that part of the rules needs to look like though. The D-Bus parts would be something like this:
# Allow communication with polkitd on the D-Bus system bus
#include <abstractions/dbus-strict>
dbus (send)
bus=system
path="/org/freedesktop/PolicyKit1/Authority"
interface="org.freedesktop.PolicyKit1.Authority"
member="{RegisterAuthenticationAgent,RegisterAuthenticationAgentWithOptions,UnregisterAuthenticationAgent,AuthenticationAgentResponse,AuthenticationAgentResponse2}"
peer=(name="org.freedesktop.PolicyKit1", label=unconfined),
dbus (send)
bus=system
path="/org/freedesktop/PolicyKit1/Authority"
interface="org.freedesktop.DBus.Properties"
peer=(name="org.freedesktop.PolicyKit1", label=unconfined),
dbus (send)
bus=system
path="/org/freedesktop/PolicyKit1/Authority"
interface="org.freedesktop.DBus.Introspectable"
member="Introspect"
peer=(name="org.freedesktop.PolicyKit1", label=unconfined),
# Allow polkitd to communicate with the agent
dbus (receive)
bus=system
interface="org.freedesktop.PolicyKit1.AuthenticationAgent"
peer=(name="org.freedesktop.PolicyKit1", label=unconfined),
# Allow agent to execute polkit-agent-helper-1
/usr/libexec/polkit-agent-helper-1 Cxr -> polkit_agent_helper,
profile polkit_agent_helper (attach_disconnected,mediate_deleted) {
...
},