Skip to content

Lock Down Permissions

mcdbus delegates all security to the operating system. It does not implement its own access control. This guide covers four independent layers you can deploy to restrict what mcdbus can do. They are all optional, all independent, and all stack on top of each other.

For the design rationale behind this approach, see Security Layers.

LayerScopeGranularityBus
systemdProcess isolationFilesystem, network, syscallsBoth
D-Bus policyMessage filteringService, interface, methodBoth
polkitAction authorizationPer-action, per-user/groupSystem (mostly)
xdg-dbus-proxySocket filteringService, interface, pathSession (mostly)

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 declarative directives.

[Unit]
Description=mcdbus D-Bus MCP server
Documentation=https://github.com/supported-systems/mcdbus
After=dbus.service
[Service]
Type=simple
ExecStart=/usr/bin/env mcdbus
DynamicUser=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectClock=yes
ProtectProc=invisible
ProcSubset=pid
ReadWritePaths=
RestrictAddressFamilies=AF_UNIX
PrivateNetwork=no
CapabilityBoundingSet=
AmbientCapabilities=
NoNewPrivileges=yes
SystemCallFilter=@system-service
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
MemoryDenyWriteExecute=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
RestrictNamespaces=yes
Environment=MCDBUS_TIMEOUT=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mcdbus
MemoryMax=256M
TasksMax=64
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target

DynamicUser=yes creates an ephemeral Unix user for each invocation. No home directory, no persistent UID, no leftover state.

Filesystem restrictions: ProtectSystem=strict makes /usr, /boot, and /efi read-only. ProtectHome=yes hides /home, /root, and /run/user entirely. PrivateTmp=yes gives the process its own /tmp.

Network restrictions: RestrictAddressFamilies=AF_UNIX limits socket creation to Unix domain sockets. D-Bus only needs AF_UNIX, so this blocks any TCP, UDP, or raw socket activity.

Capabilities: CapabilityBoundingSet= (empty) drops every Linux capability. The process cannot change file ownership, load kernel modules, bind privileged ports, or perform any other capability-gated operation.

Syscall filtering: SystemCallFilter=@system-service restricts the process to the syscall set that a typical well-behaved service needs. Things like mount, reboot, and kexec_load are blocked. Blocked syscalls return EPERM.

Memory protection: MemoryDenyWriteExecute=yes prevents mapping memory as both writable and executable, which stops most classes of code injection.

Resource limits: MemoryMax=256M and TasksMax=64 prevent runaway resource consumption.

Terminal window
sudo cp mcdbus.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now mcdbus.service

systemd ships a built-in security auditor:

Terminal window
systemd-analyze security mcdbus.service

This produces a score from 0.0 (fully exposed) to 10.0 (fully locked down). The unit file above should score above 7.0. The output lists each directive and its impact.

Check the journal for sandbox-related denials:

Terminal window
journalctl -u mcdbus.service -f

The unit file sets MCDBUS_TIMEOUT=30 by default. To override or add other variables, create a drop-in:

Terminal window
sudo systemctl edit mcdbus.service
[Service]
Environment=MCDBUS_TIMEOUT=60
Environment=MCDBUS_REQUIRE_ELICITATION=1

D-Bus itself has a message filtering layer at the bus daemon (or bus broker) level. Policy files are XML documents that control which messages any client can send or receive based on the sender’s Unix identity.

<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Default deny for mcdbus user -->
<policy user="mcdbus">
<deny send_destination="*"/>
<!-- Bus daemon (required for discovery) -->
<allow send_destination="org.freedesktop.DBus"/>
<!-- Desktop notifications -->
<allow send_destination="org.freedesktop.Notifications"
send_interface="org.freedesktop.Notifications"/>
<allow send_destination="org.freedesktop.Notifications"
send_interface="org.freedesktop.DBus.Introspectable"/>
<!-- MPRIS media players (one block per player) -->
<allow send_destination="org.mpris.MediaPlayer2.firefox"/>
<allow send_destination="org.mpris.MediaPlayer2.chromium"/>
<allow send_destination="org.mpris.MediaPlayer2.spotify"/>
<allow send_destination="org.mpris.MediaPlayer2.vlc"/>
<allow send_destination="org.mpris.MediaPlayer2.mpv"/>
<!-- UPower battery status (read-only) -->
<allow send_destination="org.freedesktop.UPower"
send_interface="org.freedesktop.DBus.Properties"
send_member="Get"/>
<allow send_destination="org.freedesktop.UPower"
send_interface="org.freedesktop.DBus.Properties"
send_member="GetAll"/>
<allow send_destination="org.freedesktop.UPower"
send_interface="org.freedesktop.UPower"
send_member="EnumerateDevices"/>
<allow send_destination="org.freedesktop.UPower"
send_interface="org.freedesktop.DBus.Introspectable"/>
<!-- bluez Bluetooth (read-only) -->
<allow send_destination="org.bluez"
send_interface="org.freedesktop.DBus.ObjectManager"
send_member="GetManagedObjects"/>
<allow send_destination="org.bluez"
send_interface="org.freedesktop.DBus.Properties"
send_member="Get"/>
<allow send_destination="org.bluez"
send_interface="org.freedesktop.DBus.Properties"
send_member="GetAll"/>
<allow send_destination="org.bluez"
send_interface="org.freedesktop.DBus.Introspectable"/>
<!-- NetworkManager (read-only) -->
<allow send_destination="org.freedesktop.NetworkManager"
send_interface="org.freedesktop.DBus.Properties"
send_member="Get"/>
<allow send_destination="org.freedesktop.NetworkManager"
send_interface="org.freedesktop.DBus.Properties"
send_member="GetAll"/>
<allow send_destination="org.freedesktop.NetworkManager"
send_interface="org.freedesktop.DBus.Introspectable"/>
<!-- systemd (list and read only) -->
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.systemd1.Manager"
send_member="ListUnits"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.DBus.Properties"
send_member="Get"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.DBus.Properties"
send_member="GetAll"/>
<allow send_destination="org.freedesktop.systemd1"
send_interface="org.freedesktop.DBus.Introspectable"/>
</policy>
</busconfig>

The policy engine evaluates rules top to bottom. The last matching rule wins. The pattern is: default deny everything, then selectively allow specific destinations, interfaces, and methods.

Each <allow> or <deny> element can filter on send_destination (service name), send_interface, send_member (method name), and send_type (message type). Combining attributes narrows the filter — an allow with both send_destination and send_member only permits that specific method on that specific service.

For the system bus:

Terminal window
sudo cp mcdbus.conf /etc/dbus-1/system.d/
sudo systemctl reload dbus.service

For dbus-broker instead of dbus-daemon:

Terminal window
sudo systemctl reload dbus-broker.service

Denied messages appear in the bus daemon journal:

Terminal window
journalctl -u dbus-broker.service --since "5 min ago" | grep -i deny

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.

polkit.addRule(function(action, subject) {
// Only apply to users in the mcdbus group
if (!subject.isInGroup("mcdbus")) {
return polkit.Result.NOT_HANDLED;
}
// Read-only systemd operations
var systemdReadActions = [
"org.freedesktop.systemd1.manage-units",
"org.freedesktop.systemd1.reload-daemon"
];
// NetworkManager: allow reading connection and device info
var nmReadActions = [
"org.freedesktop.NetworkManager.network-control",
"org.freedesktop.NetworkManager.settings.modify.system"
];
// UPower: allow reading battery status
var upowerActions = [
"org.freedesktop.upower.get-history",
"org.freedesktop.upower.get-statistics"
];
// UDisks2: allow reading disk info
var udisksReadActions = [
"org.freedesktop.udisks2.ata-smart-selftest"
];
if (systemdReadActions.indexOf(action.id) >= 0) {
if (subject.local && subject.active) {
return polkit.Result.YES;
}
}
if (nmReadActions.indexOf(action.id) >= 0) {
if (subject.local && subject.active) {
return polkit.Result.YES;
}
}
if (upowerActions.indexOf(action.id) >= 0) {
return polkit.Result.YES;
}
if (udisksReadActions.indexOf(action.id) >= 0) {
if (subject.local && subject.active) {
return polkit.Result.YES;
}
}
return polkit.Result.NOT_HANDLED;
});
Terminal window
sudo groupadd mcdbus
sudo usermod -aG mcdbus $(whoami)
# Log out and back in for the group membership to take effect
sudo cp 50-mcdbus.rules /etc/polkit-1/rules.d/
sudo systemctl restart polkit.service
Terminal window
pkcheck --action-id org.freedesktop.systemd1.manage-units \
--process $$ --allow-user-interaction

List all registered polkit actions to find the ones relevant to your deployment:

Terminal window
pkaction | grep -E 'systemd|NetworkManager|UPower|udisks'

xdg-dbus-proxy (part of Flatpak) creates a filtered D-Bus socket. You point it at the real bus socket, declare which services to expose, and it creates a new socket that only passes through matching messages. mcdbus connects to the proxy socket and never sees the rest of the bus.

#!/usr/bin/env bash
set -euo pipefail
MCDBUS_CMD="${MCDBUS_CMD:-mcdbus}"
if ! command -v xdg-dbus-proxy &>/dev/null; then
echo "error: xdg-dbus-proxy not found. Install it first." >&2
exit 1
fi
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
echo "error: DBUS_SESSION_BUS_ADDRESS is not set." >&2
exit 1
fi
PROXY_DIR="$(mktemp -d /tmp/mcdbus-proxy.XXXXXX)"
PROXY_SOCKET="${PROXY_DIR}/bus"
cleanup() {
if [ -n "${PROXY_PID:-}" ]; then
kill "$PROXY_PID" 2>/dev/null || true
wait "$PROXY_PID" 2>/dev/null || true
fi
rm -rf "$PROXY_DIR"
}
trap cleanup EXIT INT TERM
xdg-dbus-proxy "$DBUS_SESSION_BUS_ADDRESS" "$PROXY_SOCKET" \
--filter \
--talk=org.freedesktop.Notifications \
--talk=org.mpris.MediaPlayer2 \
--see=org.freedesktop.UPower \
--see=org.freedesktop.NetworkManager \
--see=org.bluez \
--see=org.freedesktop.systemd1 \
--talk=org.kde.KWin \
&
PROXY_PID=$!
for i in $(seq 1 50); do
if [ -e "$PROXY_SOCKET" ]; then
break
fi
sleep 0.1
done
if [ ! -e "$PROXY_SOCKET" ]; then
echo "error: proxy socket did not appear at $PROXY_SOCKET" >&2
exit 1
fi
export DBUS_SESSION_BUS_ADDRESS="unix:path=${PROXY_SOCKET}"
exec $MCDBUS_CMD

xdg-dbus-proxy supports three levels of access per service:

LevelEffect
--see=NAMEService appears in ListNames and can be introspected, but method calls are blocked. Read-only visibility.
--talk=NAMEFull bidirectional communication. Method calls, property reads and writes, and signals all pass through.
--own=NAMEThe client can register (own) the given bus name. mcdbus does not need this.

You can also filter at the interface and path level:

Terminal window
--call=org.freedesktop.UPower=org.freedesktop.DBus.Properties.GetAll@/org/freedesktop/UPower/*
Terminal window
sudo pacman -S xdg-dbus-proxy
Terminal window
chmod +x mcdbus-proxy.sh
./mcdbus-proxy.sh

Or integrate it into the systemd unit:

[Service]
ExecStart=/usr/local/bin/mcdbus-proxy.sh

Layer 1 alone is sufficient for most deployments. It costs nothing to deploy and provides broad process-level isolation.

Layers 1 + 2 + 3 together provide defense in depth for high-security environments or shared systems. The bus policy restricts which services mcdbus can talk to, and polkit controls which privileged operations are authorized.

Layer 4 fills the gap when you need fine-grained session bus filtering that the other layers cannot express. The trade-off is an additional proxy process.

All four layers are independent. Deploy whichever combination matches your threat model.