Three-pillar fix from Hamilton review: Code quality — validate_signature() for D-Bus spec compliance, MCDBUS_TIMEOUT env var, replace 13 error-as-success returns with ToolError, monotonic clock deadline on tree walks, sanitize D-Bus error messages, fix resource connection leak via module-level BusManager, hasattr guards in conftest. Elicitation — ctx.elicit() confirmation for system bus call_method and all set_property calls, graceful degradation when client lacks elicitation support, MCDBUS_REQUIRE_ELICITATION for hard-fail mode. Permission docs — four-layer guide (systemd sandboxing, dbus-broker policy, polkit rules, xdg-dbus-proxy) with ready-to-deploy example configs validated against xmllint, bash -n, and systemd-analyze.
11 KiB
D-Bus Permission Model
mcdbus does not implement its own access control. By design, it delegates all security decisions to the operating system's existing D-Bus permission stack. This means the same mechanisms that protect D-Bus from any other client also protect it from mcdbus.
This guide covers four independent layers that can be composed to build the level of isolation your deployment requires. They are all optional, all independent, and all stack on top of each other.
Layer 1: systemd Service Sandboxing
The single most effective thing you can do. Running mcdbus as a systemd service gives you process-level isolation using Linux namespaces, seccomp, and capability dropping -- all with a handful of declarative directives.
An example unit file is provided at docs/examples/mcdbus.service.
What it does
The unit file uses DynamicUser=yes, which creates an ephemeral Unix user
for each service invocation. No home directory, no persistent UID, no
leftover state. Combined with filesystem and network restrictions, the
mcdbus process gets a minimal sandbox:
- Filesystem:
ProtectSystem=strictmakes/usr,/boot, and/efiread-only.ProtectHome=yeshides/home,/root, and/run/userentirely.PrivateTmp=yesgives mcdbus its own/tmpthat is invisible to other processes. - Network:
RestrictAddressFamilies=AF_UNIXlimits socket creation to Unix domain sockets. D-Bus only needsAF_UNIX, so this blocks any TCP/UDP/raw socket activity. mcdbus cannot open network connections. - Capabilities:
CapabilityBoundingSet=(empty) drops every Linux capability. mcdbus cannot change file ownership, load kernel modules, bind to privileged ports, or do anything else that requires elevated privileges. - Syscalls:
SystemCallFilter=@system-servicerestricts the process to the set of syscalls that a typical well-behaved service needs. Things likemount,reboot, andkexec_loadare blocked. - Memory:
MemoryDenyWriteExecute=yesprevents mapping memory as both writable and executable. Stops most classes of code injection.
Deploying it
sudo cp docs/examples/mcdbus.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now mcdbus.service
Testing it
systemd ships a built-in security auditor:
systemd-analyze security mcdbus.service
This produces a score from 0.0 (fully exposed) to 10.0 (fully locked down). The example unit file should score above 7.0. The output also lists each directive and its impact, so you can see exactly what is and is not restricted.
Check the journal for any sandbox-related denials:
journalctl -u mcdbus.service -f
Environment variables
The unit file sets MCDBUS_TIMEOUT=30 by default. You can override this
and add other mcdbus environment variables via a drop-in:
sudo systemctl edit mcdbus.service
[Service]
Environment=MCDBUS_TIMEOUT=60
Environment=MCDBUS_REQUIRE_ELICITATION=1
Layer 2: D-Bus Bus Policy
D-Bus itself has a message filtering layer that operates at the bus daemon (or bus broker) level. This is independent of what mcdbus does -- it controls which messages any client can send or receive based on the sender's Unix identity.
An example policy file is provided at docs/examples/mcdbus.conf.
How it works
D-Bus policy files are XML documents dropped into /etc/dbus-1/system.d/
(for the system bus) or /etc/dbus-1/session.d/ (for the session bus).
Both dbus-daemon and dbus-broker read them.
The policy engine evaluates rules top to bottom. The last matching rule wins. The general pattern is:
- Default deny everything for the mcdbus user/group.
- Selectively allow specific destinations, interfaces, and methods.
Filter attributes
Each <allow> or <deny> element can filter on:
send_destination-- the bus name being called (e.g.org.freedesktop.Notifications)send_interface-- the interface being invokedsend_member-- the specific method namesend_type-- message type (method_call,signal, etc.)
You can combine these. An <allow> with both send_destination and
send_interface only permits calls to that specific interface on that
specific service.
Deploying it
For the system bus:
sudo cp docs/examples/mcdbus.conf /etc/dbus-1/system.d/
sudo systemctl reload dbus.service # dbus-daemon
# or: sudo systemctl reload dbus-broker.service
For the session bus, copy to /etc/dbus-1/session.d/ instead. Session bus
policies apply to all users. If you only want to restrict a specific user,
use user="mcdbus" in the policy context.
Testing it
Denied messages show up in the bus daemon's journal:
# dbus-broker
journalctl -u dbus-broker.service --since "5 min ago" | grep -i deny
# dbus-daemon
journalctl -u dbus.service --since "5 min ago" | grep -i deny
You can also use dbus-monitor to watch traffic in real time:
dbus-monitor --system "destination='org.freedesktop.UPower'"
Layer 3: PolicyKit (polkit)
polkit provides per-action authorization. Where D-Bus bus policy controls which messages can be sent, polkit controls whether a specific action is authorized for a specific user. Many system services (systemd, NetworkManager, UPower, udisks2) check polkit before performing privileged operations, regardless of what the D-Bus bus policy says.
An example rules file is provided at docs/examples/50-mcdbus.rules.
How it works
polkit actions are identified by reverse-domain strings like
org.freedesktop.systemd1.manage-units. When a D-Bus service receives
a method call that requires authorization, it asks polkit whether the
calling user is allowed to perform that action.
polkit rules are JavaScript files in /etc/polkit-1/rules.d/. They are
evaluated in filename order (hence the 50- prefix). Each rule function
receives an action and a subject and returns one of:
polkit.Result.YES-- allow without promptingpolkit.Result.AUTH_ADMIN-- require admin passwordpolkit.Result.NO-- deny
What the example does
The provided rules file checks if the calling user is in the mcdbus
Unix group. If so, it allows a curated set of read-only actions. Everything
else falls through to the system defaults (which typically require admin
authentication or deny outright).
Creating the group
sudo groupadd mcdbus
sudo usermod -aG mcdbus $(whoami)
# Log out and back in for the group membership to take effect
If you are running mcdbus under systemd with DynamicUser=yes, the
dynamic user will not be in any supplementary groups by default. You can
either:
- Use
SupplementaryGroups=mcdbusin the unit file, or - Use
User=mcdbuswith a real system user instead ofDynamicUser=yes
Deploying it
sudo cp docs/examples/50-mcdbus.rules /etc/polkit-1/rules.d/
sudo systemctl restart polkit.service
Testing it
List all registered polkit actions:
pkaction
Check whether a specific action would be allowed for a user:
pkcheck --action-id org.freedesktop.systemd1.manage-units \
--process $$ --allow-user-interaction
The --allow-user-interaction flag lets polkit prompt for a password if
the rules say AUTH_ADMIN. Without it, AUTH_ADMIN results are treated
as denial.
Layer 4: xdg-dbus-proxy
xdg-dbus-proxy is a Flatpak utility that creates a filtered D-Bus
socket. You point it at the real bus socket, tell it which services to
expose, and it creates a new socket that only passes through matching
messages. The client (mcdbus) connects to the proxy socket and never
sees the rest of the bus.
An example wrapper script is provided at docs/examples/mcdbus-proxy.sh.
Policy levels
xdg-dbus-proxy supports three levels of access per service:
--see=NAME-- the service appears inListNamesand can be introspected, but method calls are blocked. Read-only visibility.--talk=NAME-- full bidirectional communication with the service. Method calls, property reads/writes, and signals all pass through.--own=NAME-- the client can register (own) the given bus name. mcdbus does not need this.
You can also filter at the interface and path level:
--talk=org.freedesktop.Notifications
--call=org.freedesktop.UPower=org.freedesktop.DBus.Properties.GetAll@/org/freedesktop/UPower/*
Installing it
xdg-dbus-proxy ships with Flatpak:
# Arch
sudo pacman -S xdg-dbus-proxy
# Debian/Ubuntu
sudo apt install xdg-dbus-proxy
# Fedora
sudo dnf install xdg-dbus-proxy
How the wrapper works
The script:
- Creates a temporary directory for the proxy socket.
- Starts
xdg-dbus-proxyin the background, pointing at$DBUS_SESSION_BUS_ADDRESS. - Waits for the proxy socket to appear.
- Sets
DBUS_SESSION_BUS_ADDRESSto the proxy socket. execs mcdbus, so mcdbus runs with PID 1 semantics and gets signals correctly.- Cleans up the proxy socket and process on exit via a trap.
Deploying it
chmod +x docs/examples/mcdbus-proxy.sh
./docs/examples/mcdbus-proxy.sh
Or integrate it into the systemd unit file:
[Service]
ExecStart=/usr/local/bin/mcdbus-proxy.sh
Choosing Layers
All four layers are independent. You can use any combination.
Layer 1 (systemd sandboxing) is always recommended. It costs nothing to deploy and provides broad process-level isolation. Even if mcdbus or dbus-fast had a vulnerability, the sandbox limits what an attacker could do with it.
Layer 2 (D-Bus bus policy) is useful when you want to restrict which services mcdbus can talk to at the bus level. This is the coarsest filter but also the most reliable -- it operates inside the bus daemon itself, so there is no way for the client to bypass it.
Layer 3 (polkit) matters when the services mcdbus talks to perform their own polkit authorization checks. Most system services do this. If you want mcdbus to be able to read battery status but not manage systemd units, polkit rules are where you express that.
Layer 4 (xdg-dbus-proxy) is the most granular option. It is useful for session bus filtering (where D-Bus bus policy is less commonly configured) and for deployments where you want per-interface or per-path control. The trade-off is that it adds a proxy process.
For most deployments, Layer 1 alone is sufficient. For high-security environments or shared systems, stacking Layers 1 + 2 + 3 provides defense in depth. Layer 4 is there when you need fine-grained session bus filtering that the other layers cannot express.
Quick reference
| Layer | Scope | Granularity | Bus |
|---|---|---|---|
| systemd | Process isolation | Filesystem, network, syscalls | Both |
| D-Bus policy | Message filtering | Service, interface, method | Both |
| polkit | Action authorization | Per-action, per-user/group | System (mostly) |
| xdg-dbus-proxy | Socket filtering | Service, interface, path | Session (mostly) |