# The Basics Of polkit

> What polkit is, how it decides who can do what on a Linux machine, and how to write a rule that lets a group restart one systemd service without sudo.

- Author: Ross Edman
- Published: 2026-10-13
- Tags: linux
- URL: https://rossedman.io/blog/computers/polkit-basics/

**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.

**The subject asks**: You run `systemctl restart nginx` as yourself. `systemctl` sends a D-Bus request to systemd, which runs as root.**The mechanism checks**: systemd asks `polkitd`: can process 4242 do `org.freedesktop.systemd1.manage-units` on `nginx.service`?**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.**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.

> **Note.** Older Debian and Ubuntu releases stayed on polkit 0.105 for years and used `.pkla` files instead of JavaScript rules. If you find guides talking about `/etc/polkit-1/localauthority`, that's the old format. Ubuntu 24.04 ships polkit 124 and uses JavaScript rules like everyone else.
## 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:

```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:

| Value | Meaning |
| --- | --- |
| `no` | Denied. |
| `yes` | Allowed, no questions asked. |
| `auth_self` | The user has to type their own password. |
| `auth_admin` | An admin has to type their password. |
| `auth_self_keep` / `auth_admin_keep` | Same, 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.

> **Tip.** Don't edit the files in `/usr/share/polkit-1/actions`. Your package manager owns them and will overwrite your changes on the next update. If you want different behavior, write a rule.
## 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:

```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:

```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.

> **Compare the unit name exactly.** It's tempting to write `action.lookup("unit").indexOf("nginx") >= 0`, but that also matches a unit named `not-nginx-but-a-root-shell.service`. Match the full name, and don't let the group manage units they can also write.
## 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)](https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt), 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!
