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.

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 + 5s} in approval queue
   │  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 start with polkit-agent-h… (the kernel-truncated form of polkit-agent-helper-1). 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 (5s TTL).

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

After install, log out and back in (the XDG autostart entry is only processed at session start), then:

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

The PAM log line on success is agent_bypass: approval accepted (uid 1000).

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