fix: authorize service calls through polkit #47

Merged
mysticalsoap merged 1 commit from mysticalsoap/fix/dbus-policy into trunk 2026-08-18 21:31:31 -04:00 AGit
Owner

Problem

#18: the shipped policy's context="default" block allows send_destination="org.aqomui.service", so any local user can invoke every method on the root service — including cfgupdate (writes caller bytes to a root-owned file), change_ovpn_config (copies from caller-chosen paths), and connect_to_server/import_thread (subprocess execution from untrusted dicts).

Fix

Per-method polkit authorization, the pattern NetworkManager and friends use — the desktop user needs no group membership or any other setup:

  • systemd/org.aqomui.service.policy: new polkit action org.aqomui.service.manage, defaults allow_active=yes / allow_inactive=no / allow_any=no — the user at an active local session is allowed, everyone else (SSH, other accounts) is refused. Headless use needs a polkit rule granting the action.
  • aqomui_service.py: every D-Bus method except get_version now passes the bus sender to require_authorization(), which asks polkit CheckAuthorization (no-interaction flags, so a denied caller fails immediately instead of hanging the single-threaded service on an auth prompt). Denials raise org.aqomui.service.NotAuthorized. Internal calls (sender=None) skip the check — the bus always sets the sender for external calls. If polkit itself is unreachable the service refuses rather than allows.
  • systemd/org.aqomui.service.conf: the bus policy stays open by design so the service can identify callers — authorization lives in polkit now, and the conf says so in a comment.
  • aqomui_gui.py / aqomui_cli.py: both first-party clients report a NotAuthorized denial cleanly (the GUI notifies; the CLI prints a one-line message and exits non-zero) instead of surfacing the raw D-Bus error / a traceback.
  • Packaging: PKGBUILD installs the .policy file and gains a polkit dep (already a de-facto dep — the GUI uses pkexec); README documents the model and the headless caveat.

An earlier revision of this branch used a dedicated aqomui group in the bus policy instead; replaced wholesale after review discussion — polkit removes the usermod + re-login friction and distinguishes the physically-present user from other local accounts, which a group can't.

This is still only part of #18: server-side input validation (provider whitelisting, path canonicalization, the setattr namespace issue) remains open there.

  • aqomui-cli -t printed Successfully disconnected before it had actually issued the disconnect, so a failure (like the new deny) printed a false success followed by an error. The print now follows the call.

Verification

  • python -W error::SyntaxWarning -m compileall aqomui/ clean; CI runs pytest/ruff
  • The .policy file validates against the local polkit DTD and matches the allow_active shape of installed system actions
  • Built the branch as a package, installed it on an Arch host and restarted aqomui-service, so the checks below ran against the real polkit-gated service:
    • Authorized — from an active local session aqomui-cli -t disconnects cleanly and connecting to a VPN through the app works
    • Denied — the same command over an SSH session (a real seatless, inactive logind session) is refused with org.aqomui.service.NotAuthorized: caller is not authorized for org.aqomui.service.manage
    • The CheckAuthorization marshaling (subject/details/flags typing) and the allow_active semantics were also exercised directly against the host's live polkit daemon
  • Note on scope of allow_active: a process with no login session (e.g. a systemd --user unit) owned by a user who has an active session is authorized — polkit's documented no-session fallback. That is the authorized user's own automation acting as themselves, not the cross-user / inactive-session case #18 is about, so it's intended rather than a gap.

Refs #18

Assisted-by: claude-fable-5

## Problem [#18](https://git.mysticalsoap.com/mysticalsoap/aqomui/issues/18): the shipped policy's `context="default"` block allows `send_destination="org.aqomui.service"`, so any local user can invoke every method on the root service — including `cfgupdate` (writes caller bytes to a root-owned file), `change_ovpn_config` (copies from caller-chosen paths), and `connect_to_server`/`import_thread` (subprocess execution from untrusted dicts). ## Fix Per-method polkit authorization, the pattern NetworkManager and friends use — the desktop user needs no group membership or any other setup: - `systemd/org.aqomui.service.policy`: new polkit action `org.aqomui.service.manage`, defaults `allow_active=yes` / `allow_inactive=no` / `allow_any=no` — the user at an active local session is allowed, everyone else (SSH, other accounts) is refused. Headless use needs a polkit rule granting the action. - `aqomui_service.py`: every D-Bus method except `get_version` now passes the bus sender to `require_authorization()`, which asks polkit `CheckAuthorization` (no-interaction flags, so a denied caller fails immediately instead of hanging the single-threaded service on an auth prompt). Denials raise `org.aqomui.service.NotAuthorized`. Internal calls (`sender=None`) skip the check — the bus always sets the sender for external calls. If polkit itself is unreachable the service refuses rather than allows. - `systemd/org.aqomui.service.conf`: the bus policy stays open by design so the service can identify callers — authorization lives in polkit now, and the conf says so in a comment. - `aqomui_gui.py` / `aqomui_cli.py`: both first-party clients report a `NotAuthorized` denial cleanly (the GUI notifies; the CLI prints a one-line message and exits non-zero) instead of surfacing the raw D-Bus error / a traceback. - Packaging: PKGBUILD installs the `.policy` file and gains a `polkit` dep (already a de-facto dep — the GUI uses `pkexec`); README documents the model and the headless caveat. An earlier revision of this branch used a dedicated `aqomui` group in the bus policy instead; replaced wholesale after review discussion — polkit removes the `usermod` + re-login friction and distinguishes the physically-present user from other local accounts, which a group can't. This is still only part of #18: server-side input validation (provider whitelisting, path canonicalization, the `setattr` namespace issue) remains open there. ## Related fixes (found along the way, technically out of scope) - `aqomui-cli -t` printed `Successfully disconnected` before it had actually issued the disconnect, so a failure (like the new deny) printed a false success followed by an error. The print now follows the call. ## Verification - `python -W error::SyntaxWarning -m compileall aqomui/` clean; CI runs pytest/ruff - The `.policy` file validates against the local polkit DTD and matches the `allow_active` shape of installed system actions - Built the branch as a package, installed it on an Arch host and restarted aqomui-service, so the checks below ran against the real polkit-gated service: - **Authorized** — from an active local session `aqomui-cli -t` disconnects cleanly and connecting to a VPN through the app works - **Denied** — the same command over an SSH session (a real seatless, inactive logind session) is refused with `org.aqomui.service.NotAuthorized: caller is not authorized for org.aqomui.service.manage` - The `CheckAuthorization` marshaling (subject/details/flags typing) and the `allow_active` semantics were also exercised directly against the host's live polkit daemon - Note on scope of `allow_active`: a process with **no** login session (e.g. a `systemd --user` unit) owned by a user who has an active session is authorized — polkit's documented no-session fallback. That is the authorized user's own automation acting as themselves, not the cross-user / inactive-session case #18 is about, so it's intended rather than a gap. Refs #18 Assisted-by: claude-fable-5
fix: restrict the D-Bus policy to a dedicated aqomui group
All checks were successful
ci / test (pull_request) Successful in 30s
334a970673
The default-context policy allowed any local user to call every method
on the root service. Sends now require membership in the aqomui group
(created via sysusers.d); the GUI reports a denied caller clearly
instead of crashing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mysticalsoap changed title from fix: restrict the D-Bus policy to a dedicated aqomui group to fix: authorize service calls through polkit 2026-08-18 20:06:31 -04:00
mysticalsoap force-pushed mysticalsoap/fix/dbus-policy from 334a970673
All checks were successful
ci / test (pull_request) Successful in 30s
to 207f9c4ad4
All checks were successful
ci / test (pull_request) Successful in 27s
2026-08-18 20:26:14 -04:00
Compare
mysticalsoap force-pushed mysticalsoap/fix/dbus-policy from 207f9c4ad4
All checks were successful
ci / test (pull_request) Successful in 27s
to c34c6b34df
All checks were successful
ci / test (pull_request) Successful in 28s
ci / test (push) Successful in 46s
2026-08-18 20:50:58 -04:00
Compare
Sign in to join this conversation.
No description provided.