The Basics Of polkit

TL;DR
polkit answers one question for privileged services: can this process do this action? Actions are XML files that set the defaults, rules are JavaScript files in /etc/polkit-1/rules.d that override them, and pkaction and pkcheck let you see and test all of it from a shell.

If you’ve ever run systemctl restart without sudo and gotten a password prompt instead of an error, or clicked “Reboot” on a desktop and had it just work, you’ve used polkit. It’s one of those pieces of Linux that is everywhere and that most of us never look at until it gets in the way.

I finally sat down with it while setting up a box where a few people needed to restart one service, and nothing else. sudo can do that, but polkit turned out to be a cleaner fit, and once the model clicked it was pretty simple. In this post I’ll go over how polkit works, how to inspect it, and how to write a rule that lets a group restart a single systemd unit without a password. Let’s dive into it.


What polkit Is #

polkit (it used to be called PolicyKit) is an authorization service. It doesn’t run anything for you. It answers one question for other programs: is this process allowed to do this thing?

That’s the big difference from sudo. With sudo, you run the whole command as root. With polkit, the work is done by a service that is already privileged, like systemd, logind, NetworkManager or udisks. Your unprivileged program asks that service over D-Bus to do something, and the service asks polkit whether it should.

  1. The subject asks

    You run systemctl restart nginx as yourself. systemctl sends a D-Bus request to systemd, which runs as root.

  2. The mechanism checks

    systemd asks polkitd: can process 4242 do org.freedesktop.systemd1.manage-units on nginx.service?

  3. polkit decides

    polkitd runs its rules and answers yes, no, or “only if they authenticate”. If authentication is needed, an agent prompts the user for a password.

  4. The mechanism acts

    If the answer is yes, systemd restarts the unit. Your process never became root.

polkit’s own docs use a few words for these roles, and they’re worth knowing because they show up everywhere:

  • Subject: the process asking. polkit knows its PID, user, groups, and whether it’s in an active local login session.
  • Mechanism: the privileged service doing the work, like systemd.
  • Action: a named thing a mechanism can do, like org.freedesktop.login1.reboot.
  • Authentication agent: the thing that shows the password prompt. On a desktop it’s a GUI dialog. In a terminal it’s pkttyagent, which systemctl starts for you.

Where Everything Lives #

On Ubuntu 24.04 the pieces look like this:

  • /usr/lib/polkit-1/
    • polkitd
    • polkit-agent-helper-1
  • /usr/share/polkit-1/
    • actions/
      • org.freedesktop.login1.policy
      • org.freedesktop.systemd1.policy
    • rules.d/
      • 50-default.rules
  • /etc/polkit-1/
    • rules.d/

Actions are XML files that packages install into /usr/share/polkit-1/actions. Rules are JavaScript files in two rules.d directories. The one under /usr/share belongs to your packages. The one under /etc is yours.

Looking At Actions #

pkaction lists every action polkit knows about. A fairly minimal server has dozens:

shell
➜ pkaction --version
pkaction version 124
➜ pkaction | wc -l
91

Add --verbose and an action id to see its details:

shell
➜ pkaction --verbose --action-id org.freedesktop.login1.reboot
org.freedesktop.login1.reboot:
  description:       Reboot the system
  message:           Authentication is required to reboot the system.
  vendor:            The systemd Project
  vendor_url:        https://systemd.io
  icon:
  implicit any:      auth_admin_keep
  implicit inactive: auth_admin_keep
  implicit active:   yes
  annotation:        org.freedesktop.policykit.imply -> org.freedesktop.login1.set-wall-message

That comes straight from the XML file systemd installed:

/usr/share/polkit-1/actions/org.freedesktop.login1.policy xml
<action id="org.freedesktop.login1.reboot">
        <description gettext-domain="systemd">Reboot the system</description>
        <message gettext-domain="systemd">Authentication is required to reboot the system.</message>
        <defaults>
                <allow_any>auth_admin_keep</allow_any>
                <allow_inactive>auth_admin_keep</allow_inactive>
                <allow_active>yes</allow_active>
        </defaults>
        <annotate key="org.freedesktop.policykit.imply">org.freedesktop.login1.set-wall-message</annotate>
</action>

The <defaults> block is the interesting part. These are the implicit authorizations, and they depend on what kind of session the subject is in:

  • allow_active: the user is sitting at the machine, in the session on the active console.
  • allow_inactive: a local session that isn’t in the foreground, like a second user logged in on another VT.
  • allow_any: everything else, which includes SSH sessions and system services.

Each one is set to one of these:

ValueMeaning
noDenied.
yesAllowed, no questions asked.
auth_selfThe user has to type their own password.
auth_adminAn admin has to type their password.
auth_self_keep / auth_admin_keepSame, but polkit remembers it for a few minutes.

So the reboot action reads like this: if you’re at the keyboard, you can reboot. If you’re over SSH, an admin has to authenticate. That’s why clicking “Restart” on a laptop just works but systemctl reboot over SSH asks for a password.

Who Counts As An Admin #

auth_admin asks “an admin” to authenticate, but polkit doesn’t have a built-in idea of who that is. A rule tells it. Here’s the whole of Ubuntu’s default:

/usr/share/polkit-1/rules.d/50-default.rules js
polkit.addAdminRule(function(action, subject) {
    return ["unix-group:sudo"];
});

Fedora and Arch return unix-group:wheel instead. When an action needs auth_admin, the prompt asks for the password of someone in that group.

Rules #

Rules are where polkit gets useful. A rule is a JavaScript function that gets the action and the subject and returns a result:

js
polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.login1.reboot" &&
        subject.isInGroup("wheel")) {
        return polkit.Result.YES;
    }
});

The results match the implicit values from earlier: polkit.Result.NO, YES, AUTH_SELF, AUTH_SELF_KEEP, AUTH_ADMIN and AUTH_ADMIN_KEEP. If a rule doesn’t return anything, polkit moves on to the next one. If no rule returns anything, polkit falls back to the action’s implicit defaults.

A few things to know about how rules run:

  • Order is by filename. polkitd reads every .rules file from /etc/polkit-1/rules.d and /usr/share/polkit-1/rules.d, sorts them by filename, and runs them in that order. The first rule to return a result wins. That’s why files start with numbers. Name yours below 50- if it needs to run before the defaults.
  • /etc breaks ties. If both directories have a file with the same name, the one in /etc runs first.
  • Changes apply right away. polkitd watches both directories and reloads every rule when a file changes. No restart needed.
  • It’s ES5. Depending on how your distro built it, polkit runs rules with Duktape or SpiderMonkey. Ubuntu 24.04 uses Duktape. Stick to plain ES5: var, function, no arrow functions.

The subject has user, groups, pid, seat, session, local and active, plus isInGroup() and isInNetGroup(). The action has id and a lookup() method, which is the part I didn’t know about before.

Restarting One Service Without sudo #

Here’s the problem I actually had. A few people in an operators group need to restart nginx. They shouldn’t be able to touch anything else, and they shouldn’t need a password.

systemd uses one action for all of this, org.freedesktop.systemd1.manage-units:

shell
➜ pkaction --verbose --action-id org.freedesktop.systemd1.manage-units
org.freedesktop.systemd1.manage-units:
  description:       Manage system services or other units
  message:           Authentication is required to manage system services or other units.
  vendor:            The systemd Project
  vendor_url:        https://systemd.io
  icon:
  implicit any:      auth_admin
  implicit inactive: auth_admin
  implicit active:   auth_admin_keep

If the rule only looked at action.id, it would let operators restart every unit on the box. That’s where lookup() comes in. Mechanisms can attach details to a check, and systemd attaches the unit it’s acting on and the verb (start, stop, restart, reload and so on).

Before writing anything, let’s see what polkit says today. pkcheck asks polkit the same question a mechanism would, for any running process. I’ve made a user alice with nothing special, started a process as her, and checked it, passing the details systemd would pass:

shell
➜ pkcheck --action-id org.freedesktop.systemd1.manage-units \
    --process $(pgrep -u alice sleep) \
    --detail unit nginx.service --detail verb restart
verb=restart
unit=nginx\56service
Authorization requires authentication and -u wasn't passed.
➜ echo $?
2

pkcheck prints the details back (the \56 is an escaped .). Exit code 2 means “only with authentication”. 0 means yes, 1 means no.

Now the rule:

/etc/polkit-1/rules.d/10-operators-nginx.rules js
// Let members of "operators" start, stop and restart nginx without a password.
polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.systemd1.manage-units" &&
        action.lookup("unit") == "nginx.service" &&
        subject.isInGroup("operators")) {
        var verb = action.lookup("verb");
        if (verb == "start" || verb == "stop" || verb == "restart") {
            polkit.log("letting " + subject.user + " " + verb + " nginx");
            return polkit.Result.YES;
        }
    }
});

The file starts with 10- so it runs before the distro defaults. It only returns a result when everything matches, so any other request falls through to the normal behavior.

Testing It #

Add alice to the group and check the same process again:

shell
➜ sudo usermod -aG operators alice
➜ pkcheck --action-id org.freedesktop.systemd1.manage-units \
    --process $(pgrep -u alice sleep) \
    --detail unit nginx.service --detail verb restart
verb=restart
unit=nginx\56service
➜ echo $?
0

Exit code 0. alice can restart nginx, and she didn’t even have to log out and back in. polkit looks up the user’s groups when it does the check, not the groups the process started with, so group changes apply right away. Now let’s make sure she can’t do anything else. A different unit:

shell
➜ pkcheck --action-id org.freedesktop.systemd1.manage-units \
    --process $(pgrep -u alice sleep) \
    --detail unit postgresql.service --detail verb restart
verb=restart
unit=postgresql\56service
Authorization requires authentication and -u wasn't passed.

And a verb we didn’t allow:

shell
➜ pkcheck --action-id org.freedesktop.systemd1.manage-units \
    --process $(pgrep -u alice sleep) \
    --detail unit nginx.service --detail verb reload
verb=reload
unit=nginx\56service
Authorization requires authentication and -u wasn't passed.

Both fall back to the default: an admin has to authenticate. In real use alice just runs this and it works:

shell
alice@box:~$ systemctl restart nginx

pkcheck is the best tool I found for this. You can test a rule against any process without logging in as that user or touching the real service.

Debugging Rules #

When a rule doesn’t do what you expect, polkit.log() is your friend. It writes to the system log with the file and line number, so on a systemd machine:

shell
➜ journalctl -u polkit -f

You’ll also see syntax errors there when polkitd reloads. A rule file that fails to parse is skipped, so if your rule seems to do nothing at all, check the log first.

A Word About pkexec #

polkit also ships pkexec, which works like sudo: it runs a whole command as another user. It’s checked against its own action, which needs an admin every time:

shell
➜ pkaction --verbose --action-id org.freedesktop.policykit.exec | grep implicit
  implicit any:      auth_admin
  implicit inactive: auth_admin
  implicit active:   auth_admin

pkexec is setuid root, and that has bitten people before. In 2022 Qualys disclosed PwnKit (CVE-2021-4034), a bug that had been in pkexec since 2009 and gave any local user root on most Linux distros. It was patched quickly. But if you don’t use pkexec, it’s worth asking whether it needs to be installed at all. Ubuntu now ships it as a separate package. The rest of polkit doesn’t need it.

Conclusion #

That’s it! polkit answers one question, “can this process do this action?”, and you can read and change the answer with a couple of files and a couple of commands. Actions set the defaults, rules in /etc/polkit-1/rules.d override them, and pkcheck lets you test it all before anyone gets locked out. Next time something asks you for a password and you’re not sure why, pkaction is a good place to start. Happy hacking!