systemd Dynamic Users

last modified on

The Linux init system systemd has its controversies, but it also has some pretty neat features, my favorite of which is Dynamic Users. Lennart Poettering’s blog has a full rundown of the ins and outs, but I’ll summarize the key points below, and document a few tricks.

contents…

Users and groups and Name Service Switch, oh my!

On Linux, when you create a user it is usually added to /etc/passwd, and groups it is in are added to /etc/groups. However, these databases and others can actually come from multiple sources, controlled by the Name Service Switch, and on modern Linux one of those sources is systemd.

When configured to, systemd will create a user for the lifetime of a Unit to run that Unit. The user is never added to /etc/passwd, but exists only at runtime, and when the Unit finishes the user goes away. Although they are very locked down by default, this user is a full Linux user, and can be added to groups, write to disk, or be given capabilities. In particular, systemd provides a system for State Directories, which will be writable by the created user, persist after the Unit finishes, and will be fixed-up the next time the Unit runs to be writeable by the next dynamically created user, which can be used to store service data, or to simulate a home directory.

Simple services

A very useful result of Dynamic Users is to simplify the packaging and running of services. Instead of having to create a user to run a service when it is installed and remove it later when the service is uninstalled, the service package only needs to install a service file as the user is created at runtime. For example, below is the configuration for a web service of mine:

$ cat /lib/systemd/system/log.eth.moe
[Unit]
Description=Backend server for https://log.eth.moe

[Service]
# Create a user at runtime.
# It will have a random name.
DynamicUser=yes

# Set the user's primary group to www-data.
Group=www-data

# Create a temporary runtime directory at /run/log-eth-moe/.
RuntimeDirectory=log-eth-moe

# Create a persistent state directory at /var/lib/log-eth-moe/.
StateDirectory=log-eth-moe

ExecStart=/usr/bin/log.eth.moe -socket /run/log-eth-moe/listen.sock -dir /var/lib/log-eth-moe/

[Install]
WantedBy=multi-user.target

It’s also generally useful any time you want a non-human user to run something. For example, below is my configuration for running the Kodi media center on a Raspberry Pi:

$ cat /lib/systemd/system/kodi.service
[Unit]
Description=Kodi Media Center
After=systemd-user-sessions.service network.target sound.target

[Service]
# Create a user at runtime.
DynamicUser=yes

# Call that user "kodi".
User=kodi

# Add it to useful groups for a media center.
# Note that these are all supplementary groups,
# and the dynamic user has no primary group.
SupplementaryGroups=audio
SupplementaryGroups=input
SupplementaryGroups=plugdev
SupplementaryGroups=video

# Create a persistent state directory at /var/lib/kodi.
StateDirectory=kodi

# Set the home directory of the dynamic user to /var/lib/kodi.
Environment=HOME=/var/lib/kodi

ExecStart=/usr/bin/kodi-standalone

[Install]
WantedBy=multi-user.target

Sandboxed Steam?

Steam is great, but it also requires running other people’s code, so it’s best to sandbox it a little. While some people might go to the lengths of running Steam in Docker, I use systemd to achieve similar ends.

The steps of this approach are:

Implementation

After creating your group, grant that group permission to use your display, substituting eth-x11 for your group:

$ xhost +si:localgroup:eth-x11

This will need to be done every X11 session, and should probably be added to your .xinitrc or .xsession.

Then create a script to run Steam under systemd-run:

$ cat /usr/local/bin/steam
#!/bin/sh

set -eux

systemd-run \
    --pty \
    --property=DynamicUser=yes \
    --property=Group=eth-x11 \
    --property=SupplementaryGroups=audio \
    --property=SupplementaryGroups=input \
    --property=SupplementaryGroups=video \
    --property=StateDirectory=steam \
    --property=Environment=DISPLAY=${DISPLAY} \
    --property=Environment=HOME=/var/lib/steam \
    /usr/games/steam

Unfortunately, to use systemd-run you must either be root or authenticate yourself to systemd, but the experience can be improved with sudoers or setuid on the script.