-
Notifications
You must be signed in to change notification settings - Fork 0
Polkit 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.
[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".
| 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. |
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.
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 logAuth-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.
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 inpam_sentinel.so keeps gating sudo (if you wired it) — only the
polkit-side bypass goes back to the next module in the PAM stack.
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.
GPL-3.0-or-later. Provided as-is, without warranty of any kind (GPL §15-16). Use at your own risk on production systems.