Skip to content

Polkit Agent

Atay Özcan edited this page May 2, 2026 · 5 revisions

Polkit Authentication Agent

sentinel-polkit-agent is the per-session D-Bus service that registers with polkit so polkit consults Sentinel for graphical privilege escalation (pkexec, polkit-mediated D-Bus actions, etc). It ships since v0.4.0; v0.5.0 added BeginAuthentication serialization, an honest-cancel cancel_authentication, a 10 s timeout around polkit-agent-helper-1, and config-file respect via the shared sentinel-config crate.

How auth flows through it

[App requests privileged action]
   │
   ▼ D-Bus
[polkitd]
   │  needs authentication for action_id with cookie
   │
   ▼ D-Bus
[sentinel-polkit-agent]                  ← XDG-autostarted by the compositor
   │  1. BeginAuthentication(action_id, cookie, identities)
   │  2. spawn sentinel-helper → user clicks Allow / Deny / timeout
   │  3. on Allow: enqueue {action_id, expires_at = now + 1s} in approval queue
   │     (TTL was 5 s in v0.4; tightened in v0.5 + new `inflight` mutex
   │     serializes BeginAuthentication so a second auth can't consume
   │     the first's approval)
   │  4. connect to /run/polkit/agent-helper.socket → systemd spawns
   │     polkit-agent-helper@<n>.service running helper-1 as root
   │  5. send "<user>\n<cookie>\n"
   │
   ▼ socket
[polkit-agent-helper-1]                  ← spawned by systemd, runs PAM
   │  pam_authenticate(/etc/pam.d/polkit-1)
   │
   ▼
[pam_sentinel.so]                        ← in-process, dlopen'd by helper-1
   │  1. resolve PAM_USER → uid via passwd
   │  2. connect to $XDG_RUNTIME_DIR/sentinel-agent.sock
   │  3. read response: "OK\n" or "NO\n"
   │  4. OK → return PAM_SUCCESS  (no recursive dialog)
   │     NO → return PAM_IGNORE   (stack falls through to password prompt)

The agent's accept loop verifies each connection via SO_PEERCRED: peer uid must be 0 (root), peer comm must equal polkit-agent-helper-1 or its 15-char kernel truncation polkit-agent-he (v0.5 tightened this from a starts_with to exact match — defense in depth on top of SO_PEERCRED). On match it pops one non-expired approval from the queue and writes "OK"; otherwise "NO".

Trust model

Threat Mitigation
Same-uid attacker enqueues a fake approval Out of scope: same uid can drive polkit directly.
Same-uid attacker spoofs the agent socket Agent unlinks any pre-existing socket on startup; XDG autostart wins the bind race vs. interactive sessions.
Cross-uid attacker connects Socket mode 0600, owner = the user. Other unix users can't connect.
Spoofed polkit-agent-helper-1 Helper-1 only validates the polkit cookie via a privileged D-Bus channel back to polkitd. A spoofed helper can't supply a real cookie.
Replay across actions Approvals are one-shot (popped on first read) and short-lived (1 s TTL since v0.5; was 5 s). Combined with the per-Agent inflight serialization mutex, a second concurrent polkit auth can't consume the first's approval.

Sandbox interaction

Modern systemd-based distros run helper-1 inside polkit-agent-helper@.service with an aggressive sandbox (ProtectHome=yes, PrivateDevices, RestrictAddressFamilies=AF_UNIX, MemoryDenyWriteExecute=yes, SystemCallFilter=@system-service).

ProtectHome=yes masks /run/user/<uid> inside the sandbox — which would hide our bypass socket. Sentinel ships a unit drop-in at /etc/systemd/system/polkit-agent-helper@.service.d/sentinel.conf that disables ProtectHome for this unit. The other sandbox flags stay in place; helper-1 doesn't need anything in $HOME.

pam_sentinel.so is installed mode 0755 (not 0644) — the same sandbox refuses to dlopen .so files without the execute bit.

Verifying it's working

Since v0.5, pkexec ./install.sh restarts the running agent in place (sessionid sanity-checked, killed, re-spawned via setsid -f setpriv). You only need to log out + back in for fresh installs that didn't have an agent running yet.

pgrep -af sentinel-polkit-agent
ls -la $XDG_RUNTIME_DIR/sentinel-agent.sock
pkexec true                                      # exactly one Sentinel dialog
journalctl -t sentinel-polkit-agent -f           # live agent log
journalctl -t pam_sentinel -f                    # live PAM log

Auth-outcome events use logfmt key=value format (since v0.5):

event=auth.allow source=agent user=alice action=org.fd.policykit.exec process=topgrade latency_ms=2376
event=auth.allow source=agent.bypass action=org.fd.policykit.exec peer_pid=125021
event=auth.allow source=bypass uid=1000

Trivially queryable with journalctl ... --output=json and jq.

Disabling

sudo rm /etc/xdg/autostart/sentinel-polkit-agent.desktop
sudo rm /etc/systemd/system/polkit-agent-helper@.service.d/sentinel.conf
sudo systemctl daemon-reload
# log out + log back in

pam_sentinel.so keeps gating sudo (if you wired it) — only the polkit-side bypass goes back to the next module in the PAM stack.

Why XDG autostart and not systemctl --user

polkit's RegisterAuthenticationAgent checks that the agent's process is in the same logind session as the subject we're registering for. That session is derived from the kernel sessionid (set by audit_setloginuid at login, inherited through forks).

Compositors fork XDG autostart entries as direct children, so the agent inherits the graphical sessionid. Processes spawned via systemctl --user start are children of user@.service — a different sessionid (the manager session, not the graphical one) — and polkit refuses the registration with "Passed session and the session the caller is in differs."

Other working polkit agents (polkit-gnome, polkit-kde) ship the same way. Sentinel follows the convention.

Clone this wiki locally