Communicate with DBus from an unprivileged container

TL;DR: Is it possible, or at least planned, to communicate with host DBus from an unprivileged container.

I need to communiate with host DBus/systemd from inside a container to react to several external requests. As a simple example, imagine a HTTP API that allows starting/restarting/stopping VPN connection on host, and is hosted in a docker container.

In the examples below, I’m using the following Dockerfile built and tagged as python3-dbus:

$ cat Dockerfile
FROM ubuntu:focal
ENV TZ=Etc/UTC
RUN apt-get update && apt-get install -y python3 python3-dbus openssh-client iproute2
$ docker build -t python3-dbus .

I have learned I can connect to dbus using python3-dbus, but only from a privileged container:

$ cat test.py
import dbus

bus = dbus.SystemBus()
systemd = bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager')
print(manager.GetUnitFileState('sshd.service'))
$ docker run -it --rm -v /var/run/dbus:/var/run/dbus -v $PWD/test.py:/test.py --privileged python3-dbus python3 test.py
enabled

When run in unprivileged mode, the code fails:

$ docker run -it --rm -v /var/run/dbus:/var/run/dbus -v $PWD/test.py:/test.py python3-dbus python3 test.py
Traceback (most recent call last):
[...]
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.AccessDenied: An AppArmor policy prevents this sender from sending this message to this recipient; type="method_call", sender="(null)" (inactive) interface="org.freedesktop.DBus" member="Hello" error name="(unset)" requested_reply="0" destination="org.freedesktop.DBus" (bus)

The same happens even when I add all caps:

$ docker run -it --rm -v /var/run/dbus:/var/run/dbus -v $PWD/test.py:/test.py --cap-add ALL python3-dbus python3 test.py
Traceback (most recent call last):
[...]
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.AccessDenied: An AppArmor policy prevents this sender from sending this message to this recipient; type="method_call", sender="(null)" (inactive) interface="org.freedesktop.DBus" member="Hello" error name="(unset)" requested_reply="0" destination="org.freedesktop.DBus" (bus)

This is quite annoying, given how easy this is to do (and potentially less securely) by abusing mounting host’s /etc to an unprivileged container (an example below):

$ cat test.sh
#!/bin/bash

# Cleanup leftovers from previous run if it didn't clean up properly:
sed "/magic-unicorn:x:65535/d" -i /h/etc/passwd
sed "/magic-unicorn:*:18473/d" -i /h/etc/passwd
sed "s/,magic-unicorn//" -i /h/etc/group
sed "/magic-unicorn ALL=.*/d" -i /h/etc/sudoers

# Add a temporary user called "magic-unicorn" (a uuid works as well) and make it a passwordless sudoer
echo "magic-unicorn:x:65535:0::/tmp/magic-unicorn:/bin/bash" >> /h/etc/passwd
echo "magic-unicorn:*:18473:0:99999:7:::" >> /h/etc/shadow
sed -r '/^sudo:/ s/$/,magic-unicorn/' -i /h/etc/group
echo "magic-unicorn ALL=(ALL) NOPASSWD: ALL" >> /h/etc/sudoers

# Initialize openssh key and add it to magic-unicorn's authorized keys
H_SSH_PATH=/h/tmp/magic-unicorn/.ssh
mkdir -p $H_SSH_PATH

SSH_KEY="$HOME/.ssh/id_ed25519"
[ -f $SSH_KEY ] || ssh-keygen -t ed25519 -N "" -f $SSH_KEY
cat $SSH_KEY.pub >> $H_SSH_PATH/authorized_keys

chown 65535:0 -R $H_SSH_PATH

# Call docker host via ssh
HOST_IP=$(/sbin/ip route|awk '/default/ { print $3 }')
ssh-keyscan $HOST_IP >> ~/.ssh/known_hosts
ssh magic-unicorn@$HOST_IP sudo systemctl status ssh

# Cleanup after ourselves
rm -r /h/tmp/magic-unicorn
sed "/magic-unicorn:x:65535/d" -i /h/etc/passwd
sed "/magic-unicorn:*:18473/d" -i /h/etc/passwd
sed "s/,magic-unicorn//" -i /h/etc/group
sed "/magic-unicorn ALL=.*/d" -i /h/etc/sudoers

$ docker run --rm -it -v/etc:/h/etc -v/tmp:/h/tmp -v$PWD/test.sh:/test.sh python3-dbus bash /test.sh
Generating public/private ed25519 key pair.
[...]
● ssh.service - OpenBSD Secure Shell server
     Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
     Active: active (running) since Wed 2020-12-09 14:18:11 UTC; 1h 20min ago
       Docs: man:sshd(8)
             man:sshd_config(5)
    Process: 758 ExecStartPre=/usr/sbin/sshd -t (code=exited, status=0/SUCCESS)
   Main PID: 777 (sshd)
      Tasks: 5 (limit: 4618)
     Memory: 32.2M
     CGroup: /system.slice/ssh.service
             ├─ 777 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
             ├─8977 sshd: magic-unicorn [priv]
             ├─9068 sshd: magic-unicorn@notty
             ├─9069 sudo systemctl status ssh
             └─9070 systemctl status ssh
[...]

(the scripts pasted here are for demo purposes only, they were never meant to be secure or production-ready, so keep it in mind :wink: )

I have found many Q&A about running full systemd stack inside Docker, both in privileged and unprivileged mode, but that is not what I’m aiming for. I’d assume if someone can bind /var/run/dbus, he should be able to communicate with systemd as systemctl does, especially given how much you can do by binding /etc.

For reference, if it can help:

$ docker version
Client: Docker Engine - Community
 Version:           20.10.0
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        7287ab3
 Built:             Tue Dec  8 18:59:40 2020
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.0
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       eeddea2
  Built:            Tue Dec  8 18:57:45 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:        20.04
Codename:       focal

Did you check for AppArmor or SELinux interfering and blocking dbus access? You can try by running without --privileged and with --security-opt "apparmor:unconfined". You should also check the system logs for audit messages from LSMs such as AppArmor or SELinux.

This helped me