Linux D-Bus Security Hardening
The Problem
D-Bus is the IPC backbone of nearly every modern Linux desktop and most server distributions. Systemd uses it for unit management. NetworkManager exposes network configuration over it. PackageKit installs software through it. Polkit gates privileged operations on it. Dozens of other system services — udisks2, bluetoothd, avahi-daemon, colord, ModemManager, fprintd — expose method calls over the system bus that can mount filesystems, modify network configuration, trigger firmware updates, and authenticate users.
Every one of those method calls is a potential privilege escalation path.
The threat model is not theoretical. CVE-2021-3560 allowed a local unprivileged user to create a new administrator account by sending a single, carefully timed D-Bus message to polkit — no exploit complexity, no shellcode, just a race condition in polkit’s authentication lookup. CVE-2021-4034 (PwnKit) exploited pkexec, the polkit front-end that wraps D-Bus authentication, to achieve full root via an argv manipulation bug exploitable by any local user. Both are on CISA KEV.
The failure modes in a default installation are consistent across distributions:
- Services expose D-Bus interfaces with methods that perform privileged operations, guarded only by polkit “implicit authorization” defaults that are permissive for active local sessions.
- No logging of D-Bus method calls at the system bus level; an attacker probing interfaces generates no audit events.
- Policy files in
/usr/share/dbus-1/system.d/are vendor-supplied and rarely reviewed by operators; local overrides in/etc/dbus-1/system.d/are typically absent. - Polkit JavaScript rules in
/etc/polkit-1/rules.d/are empty on a fresh install; the effective policy comes from package-supplied.policyfiles that default toauth_admin_keepor worse,yes. - D-Bus socket activation means services that are not running are still reachable — a method call starts the service, and the entire activation sequence happens with no operator visibility.
Target systems: Systemd 240+, D-Bus 1.12+, polkit 0.105+. Distributions: RHEL/CentOS 8+, Ubuntu 20.04+, Debian 11+.
Threat Model
- Adversary 1 — Unprivileged local user exploiting polkit defaults: An attacker with a local shell sends a crafted D-Bus message to a polkit-protected service. The implicit authorization for the requested action is
auth_admin_keep. The attacker triggers a timing attack (CVE-2021-3560-style) or abuses a legitimate “active session” allowance to escalate to root without providing credentials. - Adversary 2 — RCE in a D-Bus service: A vulnerability in
networkd-dispatcher,packagekitd, or a similar service gives an attacker code execution as the service user. From there, the attacker calls further D-Bus methods on co-located services using the service’s authenticated D-Bus identity. - Adversary 3 — Confused deputy via session bus: Malicious software running as an unprivileged user hijacks a session bus service name (if registration is not properly guarded) or sends messages to a poorly-written application that trusts sender identity based on session bus proximity rather than verifying UID via
GetConnectionUnixUser. - Adversary 4 — Lateral movement through overly-permissive send rules: A policy file grants
send_destination="*"or omitssend_interfacerestrictions. An attacker who has compromised a service that is allowed to send broadly can reach interfaces intended to be internal. - Access level: Adversaries 1 and 3 require a local unprivileged shell. Adversary 2 requires application-level RCE. Adversary 4 requires any service-level compromise.
- Objective: Achieve root, install persistence, or pivot to other system services.
- Blast radius: D-Bus method calls execute synchronously under the receiver’s privileges. A single successful unauthorized call to
org.freedesktop.systemd1.Manager.StartTransientUnitororg.freedesktop.PackageKit.Transaction.InstallFilesis equivalent to arbitrary code execution as root.
Step 1: Enumerate the Attack Surface
Before restricting anything, map every service registered on the system bus.
# List all names registered on the system bus.
busctl list --system
# Show all interfaces, methods, properties, and signals
# exposed by a specific service (example: udisks2).
busctl introspect --system org.freedesktop.UDisks2 /org/freedesktop/UDisks2
# Recursively dump the entire object tree.
gdbus introspect --system \
--dest org.freedesktop.UDisks2 \
--object-path / \
--recurse
# Find all activatable service files (services started on demand).
find /usr/share/dbus-1/system-services/ /usr/lib/dbus-1/system-services/ \
-name '*.service' 2>/dev/null
# Cross-reference which of those services are currently running.
busctl list --system --no-legend \
| awk '{print $1}' \
| while read name; do
unit=$(busctl status --system "$name" 2>/dev/null | grep 'Unit:' | awk '{print $2}')
[ -n "$unit" ] && echo "$name -> $unit"
done
For each service that exposes methods affecting system state — mounts, network config, package installation, user management, firewall rules — note the interface names and the policy files that govern access to them.
# Find all D-Bus policy files.
find /usr/share/dbus-1/system.d/ \
/etc/dbus-1/system.d/ \
/usr/lib/dbus-1/system.d/ \
-name '*.conf' 2>/dev/null | sort
# Show what the polkit policy says about each action
# provided by a given service.
pkaction --verbose 2>/dev/null \
| grep -A5 'org.freedesktop.udisks2'
Step 2: Harden D-Bus Policy Files
D-Bus policy files are XML and live in /usr/share/dbus-1/system.d/ (vendor-supplied) and /etc/dbus-1/system.d/ (operator overrides; take precedence). The default policy for the system bus is deny-all for send and receive; services must explicitly open themselves. In practice, many service policy files are far too permissive.
A minimal, hardened policy for a service that only needs to receive calls from root and a dedicated service account:
<!-- /etc/dbus-1/system.d/org.example.HardenedService.conf -->
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Only the service user may own this name. -->
<policy user="examplesvc">
<allow own="org.example.HardenedService"/>
</policy>
<!-- Deny ownership to everyone else explicitly. -->
<policy context="default">
<deny own="org.example.HardenedService"/>
</policy>
<!-- Allow root to call specific methods only. -->
<policy user="root">
<allow send_destination="org.example.HardenedService"
send_interface="org.example.HardenedService.Admin"
send_member="Reload"/>
</policy>
<!-- Allow the monitoring user to read status only. -->
<policy group="monitoring">
<allow send_destination="org.example.HardenedService"
send_interface="org.freedesktop.DBus.Properties"
send_member="Get"/>
<allow send_destination="org.example.HardenedService"
send_interface="org.freedesktop.DBus.Properties"
send_member="GetAll"/>
</policy>
<!-- Deny everything else by default. -->
<policy context="default">
<deny send_destination="org.example.HardenedService"/>
</policy>
</busconfig>
Key points in policy hardening:
- Always include
send_interfaceandsend_memberinallowrules. A rule that only specifiessend_destinationpermits calls to every interface and every method on that service. - Use
<policy user="...">over<policy group="...">where possible; UID is harder to spoof than GID. - After editing, validate and reload without a full daemon restart:
# Validate XML syntax.
xmllint --noout /etc/dbus-1/system.d/org.example.HardenedService.conf
# Reload dbus-daemon policy without restarting.
systemctl reload dbus.service # systemd-based
# or on older systems:
kill -HUP $(pgrep dbus-daemon)
To audit existing vendor policy files for overly broad rules:
# Find policy files with wildcard destinations or no interface restriction.
grep -rn 'send_destination="\*"' /usr/share/dbus-1/system.d/
grep -rn '<allow send_destination=' /usr/share/dbus-1/system.d/ \
| grep -v 'send_interface'
Every hit is a service where any caller that can reach the bus can invoke any method. Write a /etc/dbus-1/system.d/ override that narrows the rule.
Step 3: Harden Polkit
Polkit is the policy layer that D-Bus services use to decide whether to honor a request from an unprivileged caller. The chain: D-Bus service receives call → queries polkit → polkit evaluates .policy file action → asks JavaScript rules in /etc/polkit-1/rules.d/ → returns yes, no, or auth_required.
The immediate hardening priority is eliminating implicit yes authorizations for privileged actions from active local sessions:
# Find all polkit actions with 'yes' or 'auth_admin_keep'
# implicit authorization (most dangerous defaults).
pkaction --verbose 2>/dev/null \
| awk '/^Action:/{action=$2} /implicit-active: (yes|auth_admin_keep)/{print action, $2}'
For actions where auth_admin_keep is too permissive (cached authentication window allows re-use without re-prompting), override with a JavaScript rule:
// /etc/polkit-1/rules.d/50-local-hardening.rules
// Require fresh admin authentication for every udisks2 mount,
// even from an active local session.
polkit.addRule(function(action, subject) {
if (action.id.indexOf("org.freedesktop.udisks2.") === 0) {
if (subject.isInGroup("wheel") || subject.isInGroup("sudo")) {
return polkit.Result.AUTH_ADMIN; // no keep; re-prompt every time
}
return polkit.Result.NO;
}
});
// Deny PackageKit installs entirely for non-wheel users.
polkit.addRule(function(action, subject) {
if (action.id.indexOf("org.freedesktop.packagekit.") === 0) {
if (!subject.isInGroup("wheel")) {
return polkit.Result.NO;
}
}
});
// Deny color profile changes (colord) for all non-root sessions.
polkit.addRule(function(action, subject) {
if (action.id.indexOf("org.freedesktop.color-manager.") === 0
&& !subject.local && !subject.active) {
return polkit.Result.NO;
}
});
Rules are evaluated in filename order; lower numbers take precedence. Place strict denials below 50 to override vendor defaults.
For CVE-2021-3560 specifically: the race was in polkit looking up the calling process’s UID — the attacker killed the requesting process before polkit finished the lookup, causing polkit to default to a privileged decision. The fix was backported into polkit 0.105-26 (RHEL) and 0.105-33 (Debian/Ubuntu). Verify:
pkaction --version
dpkg -l policykit-1 # Debian/Ubuntu
rpm -q polkit # RHEL/Fedora
Ensure polkit is at a patched version before trusting auth_admin rules.
Step 4: Systemd Service Hardening for D-Bus Consumers
Services that have no legitimate reason to communicate over D-Bus should be prevented from doing so at the systemd unit level. The Unix domain socket used by D-Bus is AF_UNIX; restricting it prevents the service from connecting to the bus entirely.
# Example hardened systemd unit for a service that
# does not need D-Bus access at all.
[Service]
ExecStart=/usr/sbin/myservice
User=myservice
Group=myservice
# Deny access to the D-Bus socket.
RestrictAddressFamilies=~AF_UNIX
# Further bus isolation (systemd 235+).
# Prevents the service from talking to systemd over D-Bus.
PrivateBus=yes # Not yet mainline; use the AF_UNIX restriction above.
# Remove ability to own D-Bus names or activate services.
SystemCallFilter=@system-service
CapabilityBoundingSet=
NoNewPrivileges=yes
For services that legitimately use D-Bus but should be limited to a specific peer:
[Service]
# Allow only AF_UNIX (D-Bus), deny network sockets.
RestrictAddressFamilies=AF_UNIX AF_NETLINK
# Prevent connecting to the session bus if only system bus is needed.
# This works by revoking access to XDG_RUNTIME_DIR socket path.
BindReadOnlyPaths=/dev/null:/run/user
Environment="DBUS_SESSION_BUS_ADDRESS=disabled:"
Socket activation hardening — when a service is started by D-Bus on demand, the activation happens through dbus-daemon. Restrict which services are activatable:
# List all system bus service activation files.
find /usr/share/dbus-1/system-services/ -name '*.service'
# A service that should not be auto-started — mask its activation file.
ln -s /dev/null /etc/dbus-1/system-services/org.freedesktop.ModemManager1.service
Masking the .service file in /etc/dbus-1/system-services/ (note: this is the D-Bus service activation file, not the systemd unit) prevents D-Bus from auto-starting the service when a method call arrives. The systemd unit can then also be masked:
systemctl mask ModemManager.service
Step 5: AppArmor Confinement for D-Bus Services
AppArmor’s D-Bus mediation (available in kernels 3.15+ with AppArmor 2.10+, enabled in Ubuntu and SUSE by default) allows restricting which bus names a process can own, which destinations it can send to, and which senders it will accept messages from.
# /etc/apparmor.d/usr.lib.example.examplesvc
#include <tunables/global>
/usr/lib/example/examplesvc {
#include <abstractions/base>
#include <abstractions/dbus-strict>
# Allow the service to own exactly one bus name.
dbus (bind)
bus=system
name="org.example.HardenedService",
# Allow responding to calls on its own interfaces.
dbus (receive)
bus=system
path=/org/example/HardenedService{,/**}
interface=org.example.HardenedService.*
peer=(label=unconfined),
# Allow receiving introspection calls from any peer.
dbus (receive)
bus=system
interface=org.freedesktop.DBus.Introspectable
member=Introspect,
# Allow calling systemd for unit status (read-only).
dbus (send)
bus=system
path=/org/freedesktop/systemd1
interface=org.freedesktop.systemd1.Manager
member={GetUnit,GetUnitByPID}
peer=(name=org.freedesktop.systemd1, label=unconfined),
# Deny all other D-Bus send/receive.
deny dbus bus=system,
deny dbus bus=session,
}
For SELinux environments, D-Bus mediation is handled through dbus_send, dbus_connect_system_bus, and dbus_connect_session_bus interface macros. An example type enforcement fragment:
# Allow myservice_t to own its bus name.
gen_require(`
type system_dbusd_t;
type dbus_home_t;
class dbus { send_msg acquire_svc };
')
allow myservice_t system_dbusd_t:dbus acquire_svc;
allow myservice_t myservice_t:dbus send_msg;
# Deny myservice_t from calling packagekitd.
# (Absence of an allow rule is sufficient under SELinux default-deny.)
Reload AppArmor profiles after changes:
apparmor_parser -r /etc/apparmor.d/usr.lib.example.examplesvc
aa-status | grep examplesvc
Step 6: Monitor D-Bus for Suspicious Activity
D-Bus produces no audit records by default. The daemon logs to the system journal but only at error level. Enabling method call monitoring requires explicit configuration.
dbus-monitor for live inspection:
# Watch all system bus traffic in real time (requires root or dbus-monitor group).
dbus-monitor --system
# Filter to a specific interface.
dbus-monitor --system \
"type='method_call',interface='org.freedesktop.systemd1.Manager'"
# Capture to file for offline analysis.
dbus-monitor --system --pcap > /tmp/dbus-$(date +%s).pcap
Enable verbose logging in dbus-daemon:
<!-- /etc/dbus-1/system-local.conf (created by operator) -->
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Log all denied messages to syslog. -->
<syslog/>
</busconfig>
systemctl reload dbus.service
# Denied sends/receives now appear in journald.
journalctl -u dbus.service -f
Correlate with auditd: D-Bus policy denials are logged with the tag dbus_denied. Add a watch:
# Watch for polkit denial events.
auditctl -a always,exit -F arch=b64 -S kill \
-F comm=polkitd -k polkit_activity
# Watch for unexpected D-Bus socket access.
auditctl -w /run/dbus/system_bus_socket -p rwa -k dbus_socket_access
eBPF-based monitoring (falco or bpftrace): For production systems where dbus-monitor overhead is unacceptable, instrument at the socket layer:
# Trace all processes connecting to the D-Bus system socket.
bpftrace -e '
tracepoint:syscalls:sys_enter_connect {
$addr = (struct sockaddr_un *)args->uservaddr;
if (str($addr->sun_path) == "/run/dbus/system_bus_socket") {
printf("%s (pid %d uid %d) connected to system bus\n",
comm, pid, uid);
}
}'
Baseline normal D-Bus activity for your workload, then alert on: new bus names being registered by unexpected UIDs, method calls to org.freedesktop.systemd1.Manager.StartTransientUnit from non-root processes, calls to PackageKit from non-interactive UIDs, and any process accessing the system bus from a context that has RestrictAddressFamilies=~AF_UNIX set (which would indicate a policy bypass).
Step 7: Disable Unnecessary D-Bus Services
The smallest attack surface is no exposed surface. On servers, the majority of D-Bus services exist to support graphical sessions and hardware management that is irrelevant to the workload.
# Services that are safe to disable on headless servers.
DISABLE_SERVICES=(
bluetooth.service # Bluetooth daemon
ModemManager.service # Mobile broadband modem management
colord.service # Color profile management
avahi-daemon.service # mDNS/DNS-SD (if not needed)
cups.service # Printing subsystem
geoclue.service # Geolocation service
switcheroo-control.service # GPU switching
wpa_supplicant.service # Wi-Fi (if wired-only server)
thermald.service # Intel thermal daemon (desktop feature)
)
for svc in "${DISABLE_SERVICES[@]}"; do
systemctl is-enabled "$svc" 2>/dev/null && \
echo "Masking $svc..." && \
systemctl mask "$svc"
done
# After masking services, purge their D-Bus activation files too.
# This prevents auto-activation if the mask is removed without also
# removing the policy file.
for svc_file in \
/usr/share/dbus-1/system-services/org.freedesktop.ModemManager1.service \
/usr/share/dbus-1/system-services/org.bluez.service \
/usr/share/dbus-1/system-services/org.freedesktop.Avahi.service; do
[ -f "$svc_file" ] && \
ln -sf /dev/null \
"/etc/dbus-1/system-services/$(basename "$svc_file")"
done
After disabling, verify the services no longer appear on the bus:
busctl list --system | grep -E 'bluetooth|avahi|ModemManager|colord'
# Should produce no output.
For services that cannot be fully disabled but expose methods you want to block, a targeted policy deny is preferable to nothing:
<!-- /etc/dbus-1/system.d/restrict-udisks2-format.conf -->
<busconfig>
<policy context="default">
<!-- Deny the Format and FormatPartition methods for non-root. -->
<deny send_destination="org.freedesktop.UDisks2"
send_interface="org.freedesktop.UDisks2.Block"
send_member="Format"/>
<deny send_destination="org.freedesktop.UDisks2"
send_interface="org.freedesktop.UDisks2.Partition"
send_member="Delete"/>
</policy>
</busconfig>
Verification
After applying hardening measures, verify the configuration is effective:
# 1. Confirm the attack surface has shrunk.
busctl list --system | wc -l # compare before/after
# 2. Test that deny rules block unauthorized calls.
# As an unprivileged user, attempt to call a restricted method.
gdbus call --system \
--dest org.freedesktop.UDisks2 \
--object-path /org/freedesktop/UDisks2 \
--method org.freedesktop.UDisks2.Manager.GetBlockDevices \
"{}"
# Should return: Error: GDBus.Error:org.freedesktop.DBus.Error.AccessDenied
# 3. Verify polkit rule overrides are active.
pkcheck --action-id org.freedesktop.udisks2.filesystem-mount \
--process $$ --allow-user-interaction 2>&1
# 4. Confirm AppArmor profile is loaded and enforcing.
aa-status | grep -A2 examplesvc
# 5. Check audit log for D-Bus socket access by unexpected processes.
ausearch -k dbus_socket_access --start today 2>/dev/null | tail -20
Key Points
- The system bus is a privilege escalation surface that is audited far less than
sudoand SSH, yet exposes equivalent power. A single authorized D-Bus call to systemd, udisks2, or PackageKit is root execution. - Polkit implicit authorizations are the highest-risk defaults. Audit every action with
implicit-active: yesand override with restrictive JavaScript rules in/etc/polkit-1/rules.d/. - D-Bus policy files should follow the principle of least-privilege: allow specific interfaces and methods by name, deny everything else. Never allow
send_destinationwithoutsend_interface. - Services with no D-Bus requirements should be blocked at the systemd unit level with
RestrictAddressFamilies=~AF_UNIX— this is a harder guarantee than a policy file deny. - On headless servers, mask Bluetooth, ModemManager, Avahi, colord, and other desktop-oriented D-Bus services. Each is an unnecessary attack surface and an unnecessary activation path.
- CVE-2021-3560 and CVE-2021-4034 are patched but their root cause — polkit’s broad default authorizations — remains in unreviewed deployments. Treat polkit policy review as mandatory, not optional.