Polkit internals¶
Developer reference for the Polkit policy type: the action catalog that
feeds the editor, how the agent turns each policy into its own JavaScript
rules file, how binding priority maps to polkit's evaluation order, and how
compliance is checked. For administrator guidance see
Polkit.
Files on the managed node¶
/etc/polkit-1/rules.d/
010-bor-a1b2c3d4.rules one file per Polkit policy that applies to the node
050-bor-e5f6a7b8.rules
PolkitRulesPath(priority, policyID) in agent/internal/policy/polkit.go
builds the name:
priorityis the policy's effective binding priority on this node (Policy.priority, the highest priority among the enabled bindings that reach the node), zero-padded to three digits.shortIDis the policy ID with every non-alphanumeric character removed, cut to 8 characters (unknownif nothing is left). The ID comes from the server and is sanitized because it becomes part of a path.
Priority: lower number wins¶
polkitd loads rules files in file-name order and evaluates the
polkit.addRule functions in that order; the first function that returns
a result decides. Because the file name starts with the binding priority,
for Polkit a lower binding priority number is evaluated first and wins.
This is the opposite of the merge-based types. In Dconf, Flatpak and the other merged types the highest priority wins, because entries are merged in ascending priority order and the last writer wins. Keep this difference in mind when changing either side; the user documentation explains it to administrators.
Further consequences of the naming:
- Priorities outside 0 to 999 do not produce three-digit names (
%03dpads but does not truncate, and negative numbers keep their sign), so they no longer sort numerically. - Two policies with the same priority are ordered by their short IDs.
- polkitd also loads files from its other rules directory
(
/usr/share/polkit-1/rules.d), sorted together with these by name.
PolkitPolicy.file_prefix in the proto is not used for the file name.
MergePolkitPolicies (an older single-file merge helper, still covered by
tests) carries it, but syncAllPolkit does not call it, and the editor
does not offer the field (see the comment in PolkitPolicyEditor.tsx).
Policy content¶
proto/policy/polkit.proto:
| Message | Fields |
|---|---|
PolkitPolicy |
rules, file_prefix (unused, see above) |
PolkitRule |
description, action_ids, action_prefixes, subject, result, action_conditions |
PolkitSubjectFilter |
in_group, negate_group, is_user, require_local, require_active, system_unit |
PolkitActionCondition |
key, value, negate |
PolkitResult |
NOT_SET, NO, YES, AUTH_SELF, AUTH_SELF_KEEP, AUTH_ADMIN, AUTH_ADMIN_KEEP (prefixed POLKIT_RESULT_) |
PolkitActionDescription |
action_id, description, message, vendor, default_any, default_inactive, default_active |
ReportPolkitCatalogueRequest / Response |
client_id, actions / success |
The policy is polkit_policy, field 15 of the typed_content oneof in
proto/policy/policy.proto. The editor stores snake_case field names, and
Polkit is listed in protoNameTypes in
server/internal/export/export.go.
The server has no type-specific validator for Polkit content:
validatePolicyContent in server/internal/services/policy.go has no
Polkit case, so any content is accepted. Structural problems surface on
the agent, where writeRuleJS refuses a rule without action IDs or
prefixes, or with result NOT_SET, and the policy is reported with status
ERROR ("failed to generate rules file").
Generated JavaScript¶
PolkitPoliciesToJS writes a header followed by one polkit.addRule call
per rule, in the order of the policy's rules:
// This file is managed by Bor. Do not edit manually.
// Changes will be overwritten by policy enforcement.
// Generated: 2026-04-11T10:00:00Z
// Allow developers to mount removable drives
polkit.addRule(function(action, subject) {
if (
action.id === "org.freedesktop.udisks2.filesystem-mount" &&
subject.isInGroup("developers")
) {
if (action.lookup("drive.removable") !== "true") { return; }
return polkit.Result.YES;
}
});
writeRuleJS builds the condition:
- Action match (any of) -
action.id === "<id>"for each entry inaction_idsandaction.id.indexOf("<prefix>") === 0for each entry inaction_prefixes. Several alternatives are wrapped in parentheses and joined with||. - Subject filter (all of) -
subject.isInGroup("<group>")(or!subject.isInGroup(...)withnegate_group),subject.user === "<user>",subject.local === true,subject.active === true,subject.system_unit === "<unit>", for each field that is set. - The action match and the subject conditions are joined with
&&. - Action conditions - inside the
if, eachaction_conditionsentry becomes an earlyreturn;whenaction.lookup(key)does not equalvalue(or equals it, withnegate). A rule that returns nothing lets polkitd continue with the next rule.
Strings are quoted with Go's %q, so quotes and backslashes in the policy
cannot break out of the JavaScript string literals.
Enforcement on the agent¶
syncAllPolkit in agent/cmd/agent/main.go runs after every change to the
set of Polkit policies and when a managed rules file is tampered with:
- Desired paths -
PolkitRulesPath(e.priority, e.id)for every cached policy. - Existing files -
ListBorManagedPolkitFiles()lists the*.rulesfiles in/etc/polkit-1/rules.dwhose content starts with// This file is managed by Bor.. - Suppress -
suppressManagedWrites(cfg, desired + existing...)anddefer updateWatcher(cfg), so the agent's own writes and deletions do not trigger a restore. - Remove stale files - every managed file that is not a desired path
is removed (
RemovePolkitRules). This covers deleted or unbound policies and priority changes, which move a policy's file to a new name. With no Polkit policies left, every managed file is removed. - Write - for each policy,
PolkitPoliciesToJSrenders the file andSyncPolkitRuleswrites it through a temporary file in the same directory,chmod 0644andrename. polkitd notices the change itself; no reload is needed. - Check and report - per policy, see below.
Keeping one file per policy means each policy's compliance is checked against its own file, deleting a policy removes exactly one file, and the evaluation order is polkitd's own file order rather than a merge inside Bor.
Tamper protection¶
getManagedPaths includes every file returned by
ListBorManagedPolkitFiles(), and onTamperedFile maps any path under
/etc/polkit-1/rules.d/ to syncAllPolkit, then reports the tamper event
with the processes that held the file. A managed file that is edited or
deleted is rewritten, and a stale managed file is removed again.
Compliance¶
CheckPolkitCompliance(policy, rulesPath) reads the file back:
- File missing or unreadable: every rule is
NON_COMPLIANT. - File identical to a freshly generated rendering: every rule is
COMPLIANT. Because the header carries a generation timestamp, this exact match is rare in practice, and the next check is the usual path. - Otherwise, each rule's own
polkit.addRuleblock is rendered and searched for in the file: found isCOMPLIANT, missing or modified isNON_COMPLIANT. - If the policy cannot be rendered at all, every rule is
ERROR.
RollupPolkitCompliance returns ERROR if any rule has an error,
otherwise NON_COMPLIANT if any rule is non-compliant, otherwise
COMPLIANT (UNKNOWN for a policy without rules). The agent reports each
policy with one item per rule:
The Compliance page recognizes the polkit: prefix and shows the rule
description instead of a schema ID. The check confirms the file content,
not that polkitd loaded it without errors.
Action catalog¶
The editor's action ID and prefix fields offer suggestions from a catalog of polkit actions stored on the server, in two tiers like the dconf schema catalog.
Built-in tier¶
server/assets/polkit_builtin_actions.json, embedded through
server/assets/polkit.go, is upserted into polkit_actions with
source = 'builtin' at every server startup by seedPolkitBuiltinActions
(server/pkg/app/polkit_seed.go, run in a goroutine). Regenerate it on a
reference system with polkit installed:
This runs agent/cmd/gen-polkit-actions, which calls pkaction --verbose
and writes the JSON. It is a manual step; commit the result.
Agent tier¶
On the first stream connection after startup (retried on later
connections until it succeeds), the agent runs DiscoverPolkitActions
(agent/internal/policy/polkit_catalogue.go), which executes
pkaction --verbose and parses one block per action:
org.freedesktop.NetworkManager.network.enable:
description: Enable or disable a network
message: System policy prevents enabling or disabling networks
vendor: NetworkManager
vendor_url: http://networkmanager.freedesktop.org/
icon:
implicit any: auth_admin
implicit inactive: auth_admin
implicit active: auth_admin
It sends the result with the ReportPolkitCatalogue RPC. The handler in
server/internal/grpc/polkit_catalogue.go upserts each action with
source = 'agent' and replaces the node's rows in node_polkit_actions.
As for dconf, an agent report never changes a builtin row's source.
Discovery is best-effort; a node without pkaction simply reports
nothing.
REST API¶
| Method | Path | Permission | Description |
|---|---|---|---|
GET |
/api/v1/polkit/actions |
policy:view |
All known actions; ?node_id=<uuid> limits the list to actions reported by that node |
Response: a JSON array of {action_id, description, message, vendor,
default_any, default_inactive, default_active}. The handler is
PolkitHandler.ListActions in server/internal/api/polkit_actions.go; the
editor loads it through fetchPolkitActions in
server/web/frontend/src/apiClient/polkitApi.ts, which also contains the
JavaScript preview generator used by the editor.
Database schema¶
Migration 000024_polkit_actions:
polkit_actions
action_id TEXT PRIMARY KEY
description TEXT
message TEXT
vendor TEXT
default_any TEXT
default_inactive TEXT
default_active TEXT
source TEXT NOT NULL DEFAULT 'agent' -- 'builtin' | 'agent'
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
node_polkit_actions
node_id UUID -> nodes(id) ON DELETE CASCADE
action_id TEXT -> polkit_actions(action_id) ON DELETE CASCADE
PRIMARY KEY (node_id, action_id)
Compliance results go to the shared compliance_results table described
in dconf internals.
File index¶
| Path | Purpose |
|---|---|
proto/policy/polkit.proto |
policy and action catalog messages |
proto/policy/policy.proto |
polkit_policy oneof field, ReportPolkitCatalogue RPC |
server/assets/polkit_builtin_actions.json, server/assets/polkit.go |
built-in catalog and its go:embed wrapper |
server/pkg/app/polkit_seed.go |
seeds the built-in catalog at startup |
server/internal/database/polkit.go |
PolkitRepository: UpsertAction, ReplaceNodeActions, ListActions, ListActionsByNode |
server/internal/grpc/polkit_catalogue.go |
ReportPolkitCatalogue handler |
server/internal/grpc/server.go |
modelToProto: case "Polkit" |
server/internal/api/polkit_actions.go |
GET /api/v1/polkit/actions |
agent/internal/policy/polkit.go |
PolkitRulesPath, PolkitPoliciesToJS, SyncPolkitRules, RemovePolkitRules, ListBorManagedPolkitFiles, CheckPolkitCompliance, RollupPolkitCompliance |
agent/internal/policy/polkit_catalogue.go |
DiscoverPolkitActions |
agent/internal/policy/polkit_test.go, polkit_catalogue_test.go |
unit tests |
agent/cmd/agent/main.go |
polkitCache, syncAllPolkit, catalog report, watcher hooks |
agent/cmd/gen-polkit-actions/main.go |
regenerates the built-in catalog |
server/web/frontend/src/apiClient/polkitApi.ts |
API client, content helpers, JavaScript preview |
server/web/frontend/src/views/Policies/PolkitPolicyEditor.tsx |
policy editor |
server/web/frontend/src/views/Compliance/CompliancePage.tsx |
compliance view (polkit: items) |