Overview
One of the key challenges the embedded device world faces is security. Undoubtedly, securing the embedded device is an arduous task and involves various layers, such as boot loaders, kernel, applications, and more. In this blog, we are going to look at how you can secure user space Linux applications in embedded devices by using Polkit (Policy Kit). Why would we use Polkit and what is it?
The Challenge
First, we need to understand the primary challenge of securing applications. Linux user space applications need higher privileges to execute operations like mounting a disk, connecting to the network, creating partitions, and more. Usually, the applications will use sudo command to perform the aforementioned operations. However the issue with this approach is that sudo provides full root privileges to the applications. These privileges can be exploited to perform unauthorized operations, thereby creating a security incident. In addition, there is no fine-grained option to control what the applications can and cannot do.
In other words, to properly secure user space Linux applications, we have to give the applications the permission to execute the operations they need to perform without exposing the embedded devices to security risks. One way we can accomplish that goal is by utilizing Polkit.
What is Polkit and how does it address these issues?
Polkit (also known as “Policy Kit”) is an application-level framework for defining and handling the security policy of the applications. Unlike with the sudo approach, the Polkit framework handles the application security in a fine-grained manner. You can also fully configure, control, and log most security-related operations in a flexible manner.
The Architecture of Polkit
Now let’s take a closer look at how Polkit functions. Primarily, the following four main components make up Polkit:
- Authentication agent. Available for both GUI and textual interface, the authentication agent validates the user by using the credentials. Depending on the needs, you can configure the scope of the security authentication agent to use the current user (or) the super user (root) credentials for the authentication. As we’ll see later on, authorization rules are defined in files with “.rules” extension.
- Polkit daemon. This is the main component in the Polkit framework. Usually, the Polkit daemon (“polkitd”) runs as part of the system startup, reads the policy files for what policies to reference, and reads the rules files to decide whether a requested action is allowed or denied. As seen in the architecture diagram below, all the components use the D-Bus (or “Desktop Bus”) inter process communication (IPC) mechanism for request and response.
- Application (subject). Linux user space applications running in the lower privilege can request the Polkit framework to carry out the actions which require security privileges. The list of actions are provided by the applications, vendors, system administrators, etc. and some typical actions would be mounting the drive, printing, opening a special device, and so on. All of these actions are defined in files with a “.policy” extension.
- Actions (mechanism). Mechanisms are privileged programs run in the system which offer services to unprivileged programs. In short, privileged programs use the authorization API provided by the Polkit framework to provide service to the unprivileged programs. pkexec is a privileged program, which is part of the Polkit framework, and allows an authorized user to execute a program as another user.
All of the above components communicate using the IPC framework D-Bus, as seen below:
What is the Polkit flow?
- First, the subject asks the mechanism to perform a privileged task.
- Then the mechanism asks polkitd for permission.
- Polkitd then checks the rules for the specified task in related files.
- If needed, polkitd will also ask the agent for authorization.
- Polkitd then tells the mechanism whether or not authorization was granted.
- If authorized, the mechanism performs the task and returns the result to the subject.
What is the Polkit “policy file”?
Written in XML format, the Polkit policy file contains the actions which the Polkit has to take for a particular request. For example, when the unprivileged user tries to mount a USB drive, the Polkit policy file indicates what action should be taken and what command should be executed as part of the action.
These set of details are explicitly described in the XML file and below is an example of the policy file:
<?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.0/policyconfig.dtd"> <policyconfig> <action id="org-opensuse-policykit-gparted"> <message>Authentication is required to run the GParted Partition Editor</message> <icon_name>gparted</icon_name> <defaults> <allow_any>auth_admin</allow_any> <allow_inactive>auth_admin</allow_inactive> <allow_active>auth_admin</allow_active> </defaults> <annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/gparted</annotate> <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> </action> </policyconfig>
A single policy file can contain multiple actions with each action identified with a unique namespace. Polkit version less than 0.106 has a different format for policy files whereas Polkit version 0.106 and above use XML format for the policy files. Throughout this blog, we’ll be using Polkit version 0.106 XML format for policy file explanation and examples.
Now back to the above example. The tags used in it are defined as follows:
- <action id> tag contains the namespace for an action. In this case, the namespace must be unique and should be compliant with the XML namespace format. New policies with relevant action details can be added based on the above given format.
- <defaults> tag contains the action permission tags <allow_any>, <allow_inactive>, <allow_active>. These tags contain the action taken by the Polkit framework. The actions and their details are given below.
Action Scope
Polkit framework can make the following actions:
Name | Description |
allow_active | valid for terminal, X sessions |
allow_inactive | valid for VNC, SSH sessions |
allow_any | valid for terminal, X, VNC, SSH sessions |
Action Types
And here are the different action types that can be used:
Name | Description |
yes | grant privilege |
no | block |
auth_self | user needs to authenticate with own password every time the privilege is requested |
auth_self_keep_session | user needs to authenticate with own password once per session, privilege is granted for the whole session |
auth_self_keep_always | user needs to authenticate with own password once, privilege is granted for the current and for future sessions |
auth_admin | user needs to authenticate with root password every time the privilege is requested |
auth_admin_keep_session | user needs to authenticate with root password once per session, privilege is granted for the whole session |
auth_admin_keep_always | user needs to authenticate with root password once, privilege is granted for the current and for future sessions |
What does the pkaction command do? The pkaction command lists out all the actions installed in the system:
$ pkaction cc.arduino.add-groups.policy com.hp.hplip.installplugin …. …. org.freedesktop.udisks2.loop-setup org.x.xf86-video-intel.backlight-helper
Now that we know what the pkaction command does, we can investigate the results further, such as how to find out more details about an individual action:
$ pkaction -v --action-id org.freedesktop.udisks2.loop-setup org.freedesktop.udisks2.loop-setup: description: Manage loop devices message: Authentication is required to set up a loop device vendor: The Udisks Project vendor_url: https://github.com/storaged-project/udisks icon: drive-removable-media implicit any: auth_admin implicit inactive: auth_admin implicit active: yes
What are Polkit authorization rules and what do they do?
Written in JavaScript, the Polkit rules files help to further secure the operations based on certain filters (such as: username, group, request name, etc.). Combining the Polkit policy and rules files in this way results in fine-grained access with control over operations requiring higher privileges.
With that in mind, let’s take a look at an example of a JavaScript rule file:
$ cat systemd-networkd.rules // Allow systemd-networkd to set timezone and transient hostname polkit.addRule(function(action, subject) { if ((action.id == "org.freedesktop.hostname1.set-hostname" || action.id == "org.freedesktop.timedate1.set-timezone") && subject.user == "systemd-network") { return polkit.Result.YES; } });
This rule checks the action.id for the “org.freedesktop.hostname1.set-hostname” or “org.freedesktop.timedate1.set-timezone” namespace string. If the username requesting this operation matches with “systemd-network”, then the return value of “polkit.Result.YES” allows the operation to proceed further.
Apart from the “polkit.Result.YES” statement, you can use other values listed in the Action Types table above and “NULL” as return values. When you use the “NULL” return value, Polkit will process the subsequent set of rules in the list.
Action Object Properties
Name | Description |
id | contains the namespace string |
lookup(string id) | returns true if the id string matches with the namespace string |
Subject Object Properties
Name | Description |
int pid | process id of the requesting application |
string user | username of the requesting application |
string[] groups | array of groups the user belongs to |
string session | The session that the subject is associated with. |
boolean active | Set to true only if the session is active. |
boolean local | Set to true only if the seat is local. |
string seat | The seat that the subject is associated with – blank if not on a local seat. |
More about rules and policies files
Different directories store the system default, third party policy, and rules files. You can see the table below for more details:
System default | Third party | |
Policy Directory | /etc/polkit-1/actions | /usr/share/polkit-1/actions |
Rules Directory | /etc/polkit-1/rules.d | /usr/share/polkit-1/rules.d |
Polkit usage
You can use the following to add Polkit support for application:
- libpolkit API’s in the application code (C++, python, etc. bindings are available too).
- pkexec application – in this case the application as such without any modifications can be used.
An example of Polkit for embedded systems
Now that we know what the Polkit policy and rules files do, let’s look at an example of a Polkit policy and rules configuration tailor-made for a Linux application keyread and running in an embedded Linux device.
An example application of the Polkit policy
The application in the below example reads the device-specific crypto key from the non-volatile storage. It is important to note that this operation requires elevated privileges. Most popular embedded build systems such as Yocto and Buildroot have Polkit packages available.
<?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.0/policyconfig.dtd"> <policyconfig> <vendor>Embedded company xyz</vendor> <vendor_url>https://www.embedded-xyz.com</vendor_url> <action id="org.company.xyz.embedded.product-name.keyread"> <description>Policy for keyread application</description> <message>Authentication is required to run the key read application</message> <icon_name>keyread</icon_name> <defaults> <allow_any>no</allow_any> <allow_inactive>no</allow_inactive> <allow_active>yes</allow_active> </defaults> <annotate key="org.freedesktop.policykit.exec.path">/bin/keyread</annotate> <annotate key="org.freedesktop.policykit.exec.allow_gui">false</annotate> </action> </policyconfig>
An example application of the Polkit rules
In the following Polkit policy for the Linux application, we have the application-specific unique namespace string “org-company-xyz-embedded-product-name-keyread”:
$ cat 00-keyread-app.rules polkit.addRule(function(action, subject) { if ((action.id == "org.company.xyz.embedded.product-name.keyread") && (subject.user == "auth_user") && (subject.local) && (subject.active)) { polkit.log(“keyread app request received”); return polkit.Result.YES; } });
We can also see that in the annotation tag, we have the application binary with a full path and that the textual application GUI option is disabled.
As far as the rule file is concerned, when the Polkit receives the request from the application, the following occurs:
- The rule checks if the namespace matches with the one in the policy file.
- The rule also confirms that the username is AuthUser.
- The rule verifies that it is executed in the local space and not through SSH, Telnet, VNC, so forth.
- And the rule checks if the user is active.
In order to track the request, the system logs it using the Polkit JavaScript object log attribute.
But how do you verify the policy using the pkaction command? Below is an example:
$ pkaction -v --action-id org-company-xyz-embedded-product-name-keyread org-company-xyz-embedded-product-name-keyread: description: Policy for keyread application message: Authentication is required to run the key read application vendor: Embedded company xyz vendor_url: https://www.embedded-xyz.com icon: keyread implicit any: no implicit inactive: no implicit active: yes annotation: org.freedesktop.policykit.exec.path -> /bin/keyread annotation: org.freedesktop.policykit.exec.allow_gui -> false
As mentioned earlier, the Polkit daemon monitors the policy and rules directories, and automatically loads, compiling the new, modified policy and rules files.
Jun 15 12:01:27 localhost polkitd[315]: Reloading rules Jun 15 12:01:27 localhost polkitd[315]: Collecting garbage unconditionally... Jun 15 12:01:27 localhost polkitd[315]: Loading rules from directory /etc/polkit-1/rules.d Jun 15 12:01:27 localhost polkitd[315]: Loading rules from directory /gnu/store/sljc6riz423v24jqh6ib6b9fiq3kdlca-polkit-0.116/share/polkit-1/rules.d Jun 15 12:01:27 localhost polkitd[315]: Finished loading, compiling and executing 5 rules
Lastly, we can use the pkexec command to validate the added policy and rule file. We can then see how the console would output this command and the Polkit logs below:
$ pkexec --user auth_user /usr/bin/keyread Key read successfully $ tail /var/log/secure Jun 15 12:55:05 localhost polkitd[315]: /etc/polkit-1/rules.d/10-keyread.rules:4: keyread app request received Jun 15 12:55:05 localhost pkexec[1478]: auth_user: Executing command [USER=auth_user] [TTY=/dev/pts/0] [CWD=/gnu/store/bhjpim6ij4zaarq0dzkijd526ahziipg-etc-polkit-1/rules.d] [COMMAND=/bin/keyread]
Conclusion
When compared to other user space hardening options, Polkit is a better choice due to the amount of flexibility it offers on the security and hardening of Linux applications.
Timesys offers an embedded Linux Yocto security layer, VigiShield Secure by Design, which includes various security features and user space hardening, and can be tailored for your security needs. Schedule a free security consultation with us to learn more.