SELinux: polkit: Authorization policy and security policy


(0 comments)

Every day you can do all daily works in the desktop environment as a normal user. Then no need to be the root, even not an administrator, you can power off the machine although really only root has capability to do so. That is because you have used the polkit authorization policy.

As an administrator, you can update passwords for users because you are authorized to an account service in the system. This service has the privileges to handle requests.

All users naturally need a network connection, authorization policy allows this by default. Administrators can even modify the network connection for all users without authentication if there is an authorization rule that allows it (this is the default authorization policy in Omarine).

Administrators can set system time and time zone based on authorization policy.

When you use a file browser, the filesystems on the drives are automatically mounted by the udisks tool, which is governed by the authorization policy.

There are many other applications that use authorization policy to create convenience for users. It also makes administration easy. However, convenience is always accompanied by security concerns. In this article we review and develop security policy in relation to authorization policy. In particular, we practice building an administrator program that updates user passwords applying authorization policy. This program is useful for administrators when the account service is not available.

Normal user, administrator, root, super user, privileged and unprivileged user, restricted user

Before going into the main part, we reinforce some concepts about users. They have different meanings depending on the specific context.
At the kernel level, there are only two kinds of user: root and normal user. root is the privileged user (technically, root has user ID of 0 so processes with an effective user ID of 0 are privileged processes) and is exempt from all kernel permission checks . Hence it is also called super user. However, since the security policy, root is no longer exempted as before and its "super" ability has also decreased. In contrast, normal users who have a user ID other than zero must undergo full permission checking and the result will, depending on their capabilities, be an unprivileged user.
It is the authorization policy that gives rise to the concept of administrator, who deal with system administration and operating tasks within the scope of the authorization policy. Thus, for authorization policy there are three kinds of user: root, administrator and normal user.
In the same view as the authorization policy, the SELinux security policy has the SELinux root corresponding to the Linux root, staff_u corresponding to the administrator and user_u corresponding to the normal user. In addition, SELinux also has users with lower rights than normal users, which can be classified as restricted user. Those are guest_u and xguest_u. Restricted users are uncommon and can be used for (mapping from) Linux system users. user_u is usually the default user.

Authorization policy

Any application that wants to apply the authorization policy needs to ship (or use) a privileged program called mechanism and an unprivileged program called subject. The mechanism is run as root or its process has the effective user ID of 0, while the subject is run as a normal user or its process has the effective user ID other than zero.

Mechanism provides the service and it can accept or deny service to requests from the subject, through the authority implemented by a polkit system daemon, polkitd

Authorization authentication is performed by an authentication agent running in the user session to check if the user of the session is the right user, administrator, or root according to the authorization policy (by entering the correct password). The agent program only provides outside input dialog, the actual authentication is done by polkit helper with program file /usr/lib/polkit-1/polkit-agent-helper-1. Although the agent runs in the user session and therefore the helper normally has no root privileges, it is set-user-ID-root to run as root and satisfies the prerequisites for checking the password

Mechanism has to declare actions that set out the default authorization. Authorization rules can be used to overwrite the default authorization or to define administration users. The communication in authorization uses the system message bus. The following is a system architecture diagram of polkit extracted from polkit(8)

Actions are those that the subject can request the mechanism to perform and are defined in XML files whose tail .policy located in /usr/share/polkit-1/actions. See polkit(8) for more details.

Authorization rules are placed in .rules files in /etc/polkit-1/rules.d and /usr/share/polkit-1/rules.d directories, written in JavaScript programming language syntax with the global object is polkit. Rule files are read by polkitd in descending order of priority by filenames.
The polkit object is of type Polkit where addRule() and addAdminRule() methods are common used, which have the following signatures:

void addRule(polkit.Result function(action, subject) {...});

void addAdminRule(string[] function(action, subject) {...});

The addRule() method is used to add a function to the general rules. The function can be called whenever an authorization check for the action and subject is performed. It should return a value from polkit.Result, for example
polkit.Result.NO: no authorization
polkit.Result.YES: allow authorization without authentication
polkit.Result.AUTH_ADMIN: Authentication by admin user is required
polkit.Result.AUTH_ADMIN_KEEP: Similar to polkit.Result.AUTH_ADMIN, but authorization is kept for a short time (this amount of time in the current version of polkit is hard-coded 5 minutes).

The addAdminRule() method is used to add a function to the admin rules. The function can be called whenever administrator authentication is required. It should return an array of strings where each string has the form "unix-group: <group>", "unix-netgroup: <netgroup>" or "unix-user: <user>".

The action parameter in the above functions is an object with information about the action being checked. It is of type Action which has an id attribute of type string

action.id: action identifier. For example "org.omarine.change-user-password".

The Action type has a method:

string lookup (string key);

The lookup() method is used to find variables passed from the mechanism. For example, the mechanism pkexec (pkexec is a mechanism available in the polkit package) sets the program variable that can be obtained using the expression action.lookup ("program") whose value is the fully qualified path of the program file that pkexec executes.

The subject parameter in the above functions is an object with information about the process being checked. It is of type Subject that has a common attribute namely user whose type of string

subject.user: username.

The Subject type has a common method:

boolean isInGroup (string groupName);

The isInGroup() method is used to check if the subject is in the groupName group.

An example of applying the authorization policy

Administrators can use the Kate editor to modify the content of a file owned by another user, even modifying a file of the root user. When you save the file, the org.kde.ktexteditor.katetextbuffer.savefile action is triggered. This action is defined in the file /usr/share/polkit-1/actions/org.kde.ktexteditor.katetextbuffer.policy. The shortened content of the file is as follows

<?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>Document Actions</vendor>
   <action id="org.kde.ktexteditor.katetextbuffer.savefile" >
      <description xml:lang="Name">Save Document</description>
      <message xml:lang="Description">Root privileges are needed to save this document</message>
      <defaults>
         <allow_inactive>no</allow_inactive>
         <allow_active>auth_admin</allow_active>
      </defaults>
   </action>
</policyconfig>

Because the default authorization is auth_admin, administrator authentication is required. This results in the handling of the rule file /etc/polkit-1/rules.d/50-default.rules because it contains the admin rule, which is the default rule of polkit

/* -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- */

// DO NOT EDIT THIS FILE, it will be overwritten on update
//
// Default rules for polkit
//
// See the polkit(8) man page for more information
// about configuring polkit.

polkit.addAdminRule(function(action, subject) {
    return ["unix-group:wheel"];
});

The function returns the wheel group so the users in the wheel group are administrators.
The requirement is to authenticate one of the administrators.
Talking about the admin group we need to know more that PAM is also involved in grouping the administrators by launching pam_wheel.so library. This library is included in the su and sudo PAM service files, so only users in the wheel group, in other words only the administrators (and root, of course) has permission to run su and sudo.
Observing above, we see that there is disagreement between KDE and polkit. KDE's intention is to allow only root to modify other user's files while polkit's default admin rule allows any administrator to act. (although technically only root can modify other user's files, in terms of authorization, root and administrator are different)

If you want to allow root only to modify other user's files, add the following rule, place in file /usr/share/polkit-1/rules.d/00-kate.rules to overwrite the default admin rule of polkit

polkit.addAdminRule(function(action, subject) {
    if (action.id == "org.kde.ktexteditor.katetextbuffer.savefile") {
        return ["unix-user:root"];
    }
});

The authentication agent now authenticate root

SELinux root is not Linux root

Linux root does not always become SELinux root and it does not if the root's process did not reach its security context.

We run the pkexec command as an administrator, pkexec allows the authorized user to execute a program as another user. If you run the command without arguments it will run /bin/bash as the root user. But the process does not transition to the root's process context but retains the administrator's process context. Linux user is root while SELinux user is staff_u

To overcome this we need to do two things:

1) Use the pam_selinux.so library in the polkit-1 PAM service to transition domain. You run the command below

sudo tee /etc/pam.d/polkit-1 << EOF

auth            include           system-auth
account         include           system-account
password        include           system-password

session         required          pam_selinux.so  close
session         required          pam_unix.so
session         required          pam_selinux.so  open

session         required          pam_systemd.so
session         optional          pam_xauth.so
EOF

2) Borrow the type sudo_exec_t of the sudo program, assign it to the program file /usr/bin/pkexec. First create the spec file

cat > spec << EOF
/usr/bin/pkexec system_u:object_r:sudo_exec_t:s0
EOF

Then set the file context

sudo setfiles spec /usr/bin/pkexec

Now root has become SELinux root

Authenticating authorization, but disallowing the user domains to read password!

During authentication, the polkit-agent-helper-1 authentication agent helper will be called from the user domain. This program needs to read the password file to authenticate. If we do not transiton the domain, the user domain will attempt to read the password, which is not acceptable in the security policy.
So the domain needs to transiton to the helper program's domain, policykit_auth_t, using the following statement in the security policy

policykit_run_auth(staff_t, staff_r)

policykit_auth_t is a specialized domain, we allow it to do

auth_read_shadow(policykit_auth_t)

Building the pwadm admin program that applies authorization policy to update user passwords

We follow the steps below to build pwadm, a program that helps administrators update user passwords. Administrators can change password for all users but root.
We define a custom action, using the mechanism pkexec. The pwadm program does not access the password file, it only encrypts the password that the user has entered and passes the encrypted password to the usermod to update the user password. Cracklib is used to check for weak passwords. The program changes the user password with a new password but is called an update because even if the password entered is the same as the old password, the encrypted password of the user is completely changed. This keeps the old password in a secure new state.

1) Writing the pwadm program
Create the source file pwadmbin.c with the following content

/* 
 * pwadm program
 * 
 * Sat 01 Aug 2020 04:10:08 AM UTC
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pwd.h>
#include <crack.h>
#include <sys/time.h>

#define false 0
#define true 1
typedef _Bool bool;
#define MAX_SALT_SIZE 16
#define MIN_SALT_SIZE 8
#define MAX_PASS_TRIES 3
#define STRCPY_SAFE(A,B) \
    (strncpy((A), (B), sizeof(A) - 1), (A)[sizeof(A) - 1] = '\0')

// Return a random number in range [min, max)

long random_range(long min, long max) {
    
    long u = (long)((double)random() / ((double)RAND_MAX + 1) 
                                        * (max - min) + min);
    return u;
}

// Set the seed for random () based on current time and process

void seed_random(void) {
    
    struct timeval tv;

    (void) gettimeofday (&tv, NULL);
    srandom ( tv.tv_sec ^ tv.tv_usec ^ getpid() );
}

// Create a salt to encrypt the user's password

const char *make_salt (size_t salt_size) {
    
    static char salt[24];
    salt[0] = '\0';
	
    do {
        strcat (salt, l64a (random()));
    } while (strlen (salt) < salt_size);

    salt[salt_size] = '\0';

    return salt;
}

// Check weak password with cracklib

bool badpass (const char *pass, const struct passwd *user) {
    
    const char *problem = FascistCheckUser(
                pass, NULL, user->pw_name, user->pw_gecos);
				
    if (problem != NULL) {
        printf ("Bad password: %s.\n", problem);
        return true;
    }
	
    return false;
}

int
main(int argc, char *argv[]) {
    
    char *encrypted_pass, *clear_pass;
    char *username, *prog;
    struct passwd *user;
    int ret, i;
    
    char salt[21];
    size_t salt_len;
    char pass[200];
    
    prog = argv[0];
    if (argc != 2) {
        fprintf(stderr, "Usage: %s username\n", prog);
        exit(EXIT_FAILURE);
    }
    
    username = argv[1];
    // Administrator can change the password of every user except root
    if (strcmp (username, "root") == 0) {
        fprintf (stderr, \
        "%s: you are not authorized to change the password of \
the root user.\n", prog);
        exit(EXIT_FAILURE);
    }
    
    // Check account name
    errno = 0;
    user = getpwnam(username);
    if (! user) {
        if (errno)
            perror("getpwnam");
        else
            fprintf (stderr, "%s: user not found.\n", prog);
        exit(EXIT_FAILURE);
    }
    
    // Let administrator enter new password up to MAX_PASS_TRIES times
    bool warned = false;
    for (i = 0; i < MAX_PASS_TRIES; i++) {
        clear_pass = getpass ("New password: ");
        if (! clear_pass) {
            perror("getpass");
            exit(EXIT_FAILURE);
        }
        if (warned && (strcmp (pass, clear_pass) != 0)) {
            warned = false;
        }
        STRCPY_SAFE (pass, clear_pass);
        bzero (clear_pass, strlen(clear_pass));

        if (! warned && badpass (pass, user)) {
            if (i < MAX_PASS_TRIES - 1) {
                printf ("\n%s\n", \
                "Warning: weak password. Enter it again if you still \
want to use it.");
                warned = true;
            }
            continue;
        }
        clear_pass = getpass ("Re-enter new password: ");
        if (! clear_pass) {
            perror("getpass");
            exit(EXIT_FAILURE);
        }
        if (strcmp (clear_pass, pass) != 0) {
            printf ("\n%s\n", "Passwords do not match please try again.");
        } else {
            bzero (clear_pass, strlen(clear_pass));
            break;
        }
    }

    if (i == MAX_PASS_TRIES) {
        fprintf (stderr, "The password for %s has not changed.\n", username);
        exit(EXIT_FAILURE);
    }
    
    // Seed for random ()
    seed_random();

    // Make the salt
    strcpy (salt, "$6$");
    salt_len = (size_t) random_range (MIN_SALT_SIZE, MAX_SALT_SIZE + 1);
    strcat (salt, make_salt (salt_len));
    strcat (salt, "$");
    
    // Encrypt the password
    encrypted_pass = crypt (pass, salt);
    bzero (pass, sizeof pass);
    if (! encrypted_pass) {
        fprintf (stderr, "%s: password encryption failed.\n", prog);
        exit(EXIT_FAILURE);
    }
    
    // Update the encrypted password for username
    char *args[6];
    args[0] = "/usr/sbin/usermod";
    args[1] = "-p";
    args[2] = encrypted_pass;
    args[3] = "--";
    args[4] = username;
    args[5] = NULL;
    
    ret = execvp("/usr/sbin/usermod", args); 
    if (ret == -1) {
        perror("execvp");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}

2) Compiling the program
You compile the program as follows

gcc -lcrypt -lcrack -o pwadmbin pwadmbin.c

3) Copy the pwadmbin binary to /usr/bin

sudo cp pwadmbin /usr/bin

4) Create the pwadm script program

sudo tee /usr/bin/pwadm << "EOF”
#!/usr/bin/pkexec /bin/bash

pwadmbin $*
EOF

5) Turn the script file into an executable file

sudo chmod u+x /usr/bin/pwadm

6) Do the same as pkexec, set pwadm file type to sudo_exec_t

7) Create org.omarine.change-user-password action defined in /usr/share/polkit-1/actions/org.omarine.policy as follows

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD polkit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/software/polkit/policyconfig-1.dtd">
<policyconfig>

  <vendor>pwadm</vendor>
  <vendor_url>https://www.omarine.org</vendor_url>

  <action id="org.omarine.change-user-password">
    <description>Change user passwords</description>
    <message>Authentication is required to change user password</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">/bin/bash</annotate>
    <annotate key="org.freedesktop.policykit.exec.argv1">/usr/bin/pwadm</annotate>
  </action>

</policyconfig>

8) Getting details about the action
You run the command below to see action details

pkaction --action-id org.omarine.change-user-password --verbose

Now we can run the program

pwadm tho

Because the default authorization is auth_admin_keep, we can work with user passwords within 5 minutes without having to re-authenticate.

Unlike account service as a mechanism, the pwadm program is a subject. The account service updates the requested password from a  subject for example named user manager. The user manager receives the password entered from the user, encrypts it and then sends it to the account service. These operations are conducted as the user of the session and require absolutely no root privileges. Account service then updates the password based on its root privileges. While the pwadm program does not inherently have the privileges but after authorization it is run as root and becomes privileged. It does all the works, from receiving password, encryption to updating password.

Questions for you

1) As a mechanism, the account service performs a specific action of updating password. So what action does pkexec take?

2) Is the pwadm program secure?
Hint: see org.omarine.change-user-password action

3) When running pwadm, how does the domain transition chain take place?
Hint: when the kernel loads the script, it replaces the script with the interpreter. But the script determines the security context, not the interpreter program. The sequence of executables seen in the user space is /usr/bin/pkexec → /bin/bash → /usr/bin/pwadmbin

Currently unrated

Comments

There are currently no comments

New Comment

required

required (not published)

optional

required


What is 5 - 5?

required