Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Linux Admins By James Joyner IV · · 10 min read

Writing polkit Rules on Linux: Fine-Grained Privilege Without sudo Sprawl

polkit decides who may do privileged desktop and systemd actions. Learn to read and write polkit rules safely, and use AI to decode actions and avoid over-granting.

  • #linux
  • #ai
  • #polkit
  • #authorization
  • #security
  • #systemd

A developer asks for the ability to restart one specific systemd service without a password. The lazy answer is a sudoers line — or worse, full sudo. But the request isn’t really about the shell; that service is being managed through systemd’s D-Bus interface, and the right place to grant exactly that one action, to exactly that one group, is polkit. Most admins never learn it, so they reach for sudo and end up with privilege sprawl nobody can audit.

polkit (formerly PolicyKit) is the authorization layer that sits between unprivileged processes and privileged operations exposed over D-Bus — package installs, service management, mounting disks, network config, reboots. It lets you answer “is this subject allowed to do this action under these conditions?” with surgical precision. Here’s how I read and write polkit rules, and where an AI copilot helps decode the action namespace and keep the grant minimal.

The mental model: actions, subjects, results

Three concepts and you’ve got it:

  • Action — a registered privileged operation, named like org.freedesktop.systemd1.manage-units or org.freedesktop.NetworkManager.network-control. Every privileged D-Bus operation declares one.
  • Subject — who’s asking: a user, a group, whether they’re on an active local session, etc.
  • Result — what polkit decides: yes, no, auth_admin (prompt for an admin password), auth_self (prompt for the user’s own password), and _keep variants that cache the answer.

Rules evaluate against an action plus subject and return a result. That’s the whole engine.

Finding the action you actually need

The first real skill is discovering the precise action ID, because granting the right narrow action instead of a broad one is the entire point.

# List every registered action and its default policy
pkaction

# Inspect one action in detail
pkaction --action-id org.freedesktop.systemd1.manage-units --verbose

# Watch which action fires when you attempt the operation
# (run the operation in another terminal, watch the journal)
journalctl -f -u polkit

That pkaction --verbose output shows the default allow_active/allow_inactive/allow_any results — which is what you’re about to override for a specific subject.

Writing a rule (the JavaScript way)

Modern polkit rules live in /etc/polkit-1/rules.d/ as JavaScript. Here’s a rule letting the deploy group restart only the myapp.service unit without a password, while everything else stays default:

// /etc/polkit-1/rules.d/49-deploy-myapp.rules
polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.systemd1.manage-units" &&
        subject.isInGroup("deploy")) {
        var unit = action.lookup("unit");
        if (unit == "myapp.service") {
            var verb = action.lookup("verb");
            if (verb == "start" || verb == "stop" || verb == "restart") {
                return polkit.Result.YES;
            }
        }
    }
    // Fall through: everything else uses the default policy
});

Note what this does not do: it doesn’t return polkit.Result.NO for the catch-all case. Returning nothing means “I have no opinion, use the default,” which is exactly what you want — a rule that grants one narrow thing and stays silent on everything else. The file prefix (49-) controls evaluation order; lower numbers win.

# polkit picks up rules.d changes without a restart, but confirm:
sudo systemctl restart polkit   # if needed on your distro
# Then test as the deploy user:
systemctl restart myapp.service   # should not prompt
systemctl restart sshd            # should still prompt / deny

The over-granting traps

polkit’s power is also its risk. The two ways people blow their foot off:

  • Granting the whole action. Returning YES for org.freedesktop.systemd1.manage-units without checking the unit lookup hands the subject control over every systemd unit — including overwriting your security units. Always constrain by action.lookup("unit") and verb.
  • subject.local vs remote. A rule that only checks group membership applies over SSH too. If you meant “only at the physical console,” you must also check subject.local and subject.active. Forgetting this can turn a desktop-convenience rule into a remote-privilege grant.

Where AI earns its place

The polkit action namespace is large, sparsely documented, and the JavaScript API’s lookup() keys differ per action. That’s exactly the kind of decode-and-draft work I hand to an AI copilot — then review.

Prompt: You are a senior Linux security engineer. I want members of the storage-ops group to mount and unmount removable disks via udisks2 without a password, but ONLY when they’re on an active local session, and nothing else. Give me the polkit action ID(s), the rules.d JavaScript, and explicitly call out anything that would over-grant. Then tell me how to verify it’s scoped correctly.

Response (excerpt): The relevant actions are org.freedesktop.udisks2.filesystem-mount and ...filesystem-unmount. Constrain with subject.isInGroup("storage-ops") && subject.local && subject.active so the grant does NOT apply over SSH. Do not grant ...filesystem-mount-system (internal/fixed disks) — that’s the over-grant trap; keep it to the removable-device actions. Verify: as a storage-ops user at the console, mount a USB stick (no prompt); then SSH in and retry (should prompt/deny), proving the local-session scope holds.

polkit.addRule(function(action, subject) {
    if ((action.id == "org.freedesktop.udisks2.filesystem-mount" ||
         action.id == "org.freedesktop.udisks2.filesystem-unmount") &&
        subject.isInGroup("storage-ops") &&
        subject.local && subject.active) {
        return polkit.Result.YES;
    }
});

That’s the ideal division of labor: the model knew the udisks2 action IDs, flagged the mount-system over-grant I’d never have thought to exclude, and built the local-session scoping in. But it also handed me the test that proves the scope — SSH in and confirm the rule does NOT fire. I ran that test before trusting the rule. AI decodes the namespace and drafts the rule; the human verifies the scope by trying to break it.

The takeaway

polkit is the precise privilege tool Linux admins keep reaching past in favor of sudo, and that habit produces privilege sprawl nobody can audit. Once you internalize the action / subject / result model and the discipline of returning YES for one narrow, constrained action while staying silent on everything else, you can grant exactly what’s asked — one service, one verb, one group, optionally console-only — instead of handing out a shell. AI is genuinely valuable for decoding the sprawling action namespace and catching over-grant traps like mount-system or missing subject.local checks. But the rule isn’t done until you’ve tested that it grants the narrow thing and refuses everything else, including over SSH. The model drafts; you verify by trying to break the scope.

For adjacent privilege work, see managing sudo and Linux permissions without footguns and replacing setuid root with fine-grained capabilities. The sudoers policy authoring and validation prompt pairs naturally with a polkit review when you’re deciding which tool fits a grant.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.