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-unitsororg.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_keepvariants 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
YESfororg.freedesktop.systemd1.manage-unitswithout checking theunitlookup hands the subject control over every systemd unit — including overwriting your security units. Always constrain byaction.lookup("unit")andverb. subject.localvs remote. A rule that only checks group membership applies over SSH too. If you meant “only at the physical console,” you must also checksubject.localandsubject.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-opsgroup 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-mountand...filesystem-unmount. Constrain withsubject.isInGroup("storage-ops") && subject.local && subject.activeso 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.
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.