mcdbus/docs/permissions.md
Ryan Malloy 5fa1eb36ef Hamilton remediation: validation, ToolError, elicitation, permission docs
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.
2026-03-06 11:54:31 -07:00

333 lines
11 KiB
Markdown

# 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](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=strict` makes `/usr`, `/boot`, and `/efi`
read-only. `ProtectHome=yes` hides `/home`, `/root`, and `/run/user`
entirely. `PrivateTmp=yes` gives mcdbus its own `/tmp` that is invisible
to other processes.
- **Network:** `RestrictAddressFamilies=AF_UNIX` limits socket creation to
Unix domain sockets. D-Bus only needs `AF_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-service` restricts the process to
the set of syscalls that a typical well-behaved service needs. Things
like `mount`, `reboot`, and `kexec_load` are blocked.
- **Memory:** `MemoryDenyWriteExecute=yes` prevents mapping memory as both
writable and executable. Stops most classes of code injection.
### Deploying it
```bash
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:
```bash
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:
```bash
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:
```bash
sudo systemctl edit mcdbus.service
```
```ini
[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](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:
1. Default deny everything for the mcdbus user/group.
2. 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 invoked
- `send_member` -- the specific method name
- `send_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:
```bash
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:
```bash
# 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:
```bash
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](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 prompting
- `polkit.Result.AUTH_ADMIN` -- require admin password
- `polkit.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
```bash
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=mcdbus` in the unit file, or
- Use `User=mcdbus` with a real system user instead of `DynamicUser=yes`
### Deploying it
```bash
sudo cp docs/examples/50-mcdbus.rules /etc/polkit-1/rules.d/
sudo systemctl restart polkit.service
```
### Testing it
List all registered polkit actions:
```bash
pkaction
```
Check whether a specific action would be allowed for a user:
```bash
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](examples/mcdbus-proxy.sh).
### Policy levels
xdg-dbus-proxy supports three levels of access per service:
- `--see=NAME` -- the service appears in `ListNames` and 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:
```bash
--talk=org.freedesktop.Notifications
--call=org.freedesktop.UPower=org.freedesktop.DBus.Properties.GetAll@/org/freedesktop/UPower/*
```
### Installing it
xdg-dbus-proxy ships with Flatpak:
```bash
# 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:
1. Creates a temporary directory for the proxy socket.
2. Starts `xdg-dbus-proxy` in the background, pointing at `$DBUS_SESSION_BUS_ADDRESS`.
3. Waits for the proxy socket to appear.
4. Sets `DBUS_SESSION_BUS_ADDRESS` to the proxy socket.
5. `exec`s mcdbus, so mcdbus runs with PID 1 semantics and gets signals correctly.
6. Cleans up the proxy socket and process on exit via a trap.
### Deploying it
```bash
chmod +x docs/examples/mcdbus-proxy.sh
./docs/examples/mcdbus-proxy.sh
```
Or integrate it into the systemd unit file:
```ini
[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) |