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:
- Create a group to share your display with. For myself,
eth
, I created eth-x11
. - Allow members of that group to access your display.
- Run Steam as a member of that group, directed to use your display.
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
:
- Run the command with a TTY so that Steam’s log messages are output to your terminal for debugging.
- Create a user at runtime.
- Set the primary group of the user to be
eth-x11
. The xhost
command above requires it to be the primary group. - Add the user to the groups
audio
, input
, and video
. - Create a
StateDirectory
called steam
, which systemd
will put at /var/lib/steam/
. - Set the
$DISPLAY
to be the current X11 display. - Set the
$HOME
of the running process to /var/lib/steam/
.
$ 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.