wezm.net/v2/content/posts/2023/systemd-sysusers-and-chimera-linux.md
2023-12-19 08:16:45 +10:00

19 KiB

+++ title = "systemd-sysusers and Chimera Linux" date = 2023-12-18T11:25:57+10:00

#[extra] #updated = 2023-01-11T21:11:28+10:00 +++

I use Chimera Linux as the primary OS on my laptop (as opposed to my desktop, which is still running Arch Linux for now). Chimera was created in 2021 and reached alpha status in June 2023. Chimera was built from scratch and as the name suggests it comprised of a motley crew of components:

The project and its development is proving very useful to me for seeing how a Linux distribution is built and evolved over time. Watching it progress (and helping a little by maintaining some packages) has helped expose some lesser known (to me) components that make up a typical Linux system, and their role.

Recently systemd-sysusers was introduced. Some folks might find this surprising as Chimera does not use systemd for the role of pid 1/init. As mentioned above it uses Dinit for this. Some standalone parts of systemd are used though. Currently:

  • udev
  • systemd-tmpfiles
  • and now, systemd-sysusers

I had not encountered systemd-sysusers previously (even though it's probably used on the systemd based distros I've used before), so I thought I'd jot down what I learned about it and how it's used (at the time of writing) in Chimera.

What is systemd-sysusers?

As the name implies systemd-sysusers is designed to make managing system users (and groups) easier. System users are users that a typically created for system processes to use for privilege separation. For example, the CUPS printing system runs as the _cups user.

In the commit that introduced sysusers into systemd Lennart Poettering included this description:

systemd-sysusers is a tool to reconstruct /etc/passwd and /etc/group from static definition files that take a lot of inspiration from tmpfiles snippets. These snippets should carry information about system users only. To make sure it is not misused for normal users these snippets only allow configuring UID and gecos field for each user, but do not allow configuration of the home directory or shell, which is necessary for real login users.

The purpose of this tool is to enable state-less systems that can populate /etc with the minimal files necessary, solely from static data in /usr. systemd-sysuser is additive only, and will never override existing users.

This tool will create these files directly, and not via some user database abtsraction layer. This is appropriate as this tool is supposed to run really early at boot, and is only useful for creating system users, and system users cannot be stored in remote databases anyway.

The tool is also useful to be invoked from RPM scriptlets, instead of useradd. This allows moving from imperative user descriptions in RPM to declarative descriptions.

The UID/GID for a user/group to be created can either be chosen dynamic, or fixed, or be read from the owner of a file in the file system, in order to support reconstructing the correct IDs for files that shall be owned by them.

This also adds a minimal user definition file, that should be sufficient for most basic systems. Distributions are expected to patch these files and augment the contents, for example with fixed UIDs for the users where that's necessary.

Chimera is using it for the scenario described for RPM. Specifically snippets in /usr/lib/sysusers.d are used to describe the system users and groups that should exist.

systemd-sysusers in Chimera Linux

Before the introduction of systemd-sysusers packages declared system users and groups that were required in the package template.py and cbuild would generate scripts from a template that added the users and groups, as well as disable them when packages are uninstalled. These scripts had to handle things like the user/group already existing, tools for adding users or groups missing, failures creating users/groups, etc.

The generated scripts were then embedded into the final apk package as scripts tied to "pre-install", "pre-upgrade", and "post-deinstall" actions.

For example, here is the scripts section (formatted by adbdump as YAML) for the chrony NTP client/server:

scripts:
  pre-install: |
    #!/bin/sh
    
    _chrony_homedir=/var/lib/chrony
    system_users=_chrony
    
    _system_accounts_invoke() {
    
        local USERADD USERMOD
    
        [ -z "$system_users" -a -z "$system_groups" ] && return 0
    
        if command -v useradd >/dev/null 2>&1; then
            USERADD="useradd"
        fi
    
        if command -v usermod >/dev/null 2>&1; then
            USERMOD="usermod"
        fi
    
        show_acct_details() {
            echo "   Account: $1"
            echo "   Description: '$2'"
            echo "   Homedir: '$3'"
            echo "   Shell: '$4'"
            [ -n "$5" ] && echo "   Additional groups: '$5'"
        }
    
        group_add() {
            local _pretty_grname _grname _gid
    
            if ! command -v groupadd >/dev/null 2>&1; then
                echo "WARNING: cannot create $1 system group (missing groupadd)"
                echo "The following group must be created manually: $1"
                return 0
            fi
    
            _grname="${1%:*}"
            _gid="${1##*:}"
    
            [ "${_grname}" = "${_gid}" ] && _gid=
    
            _pretty_grname="${_grname}${_gid:+ (gid: ${_gid})}"
    
            groupadd -r ${_grname} ${_gid:+-g ${_gid}} >/dev/null 2>&1
    
            case $? in
                0) echo "Created ${_pretty_grname} system group." ;;
                9) ;;
                *) echo "ERROR: failed to create system group ${_pretty_grname}!"; return 1;;
            esac
    
            return 0
        }
    
        # System groups required by a package.
        for grp in ${system_groups}; do
            group_add $grp || return 1
        done
    
        # System user/group required by a package.
        for acct in ${system_users}; do
            _uname="${acct%:*}"
            _uid="${acct##*:}"
    
            [ "${_uname}" = "${_uid}" ] && _uid=
    
            eval homedir="\$${_uname}_homedir"
            eval shell="\$${_uname}_shell"
            eval descr="\$${_uname}_descr"
            eval groups="\$${_uname}_groups"
            eval pgroup="\$${_uname}_pgroup"
    
            [ -z "$homedir" ] && homedir="/var/empty"
            [ -z "$shell" ] && shell="/usr/bin/nologin"
            [ -z "$descr" ] && descr="${_uname} user"
            [ -n "$groups" ] && user_groups="-G $groups"
    
            if [ -n "${_uid}" ]; then
                use_id="-u ${_uid} -g ${pgroup:-${_uid}}"
                _pretty_uname="${_uname} (uid: ${_uid})"
            else
                use_id="-g ${pgroup:-${_uname}}"
                _pretty_uname="${_uname}"
            fi
    
            if [ -z "$USERADD" -o -z "$USERMOD" ]; then
                echo "WARNING: cannot create ${_uname} system account (missing useradd or usermod)"
                echo "The following system account must be created:"
                show_acct_details "${_pretty_uname}" "${descr}" "${homedir}" "${shell}" "${groups}"
                continue
            fi
    
            group_add ${pgroup:-${acct}} || return 1
    
            ${USERADD} -c "${descr}" -d "${homedir}" \
                ${use_id} ${pgroup:+-N} -s "${shell}" \
                ${user_groups} -r ${_uname} >/dev/null 2>&1
    
            case $? inhttps://github.com/systemd/systemd/commit/1b99214789101976d6bbf75c351279584b071998
                0)
                    echo "Created ${_pretty_uname} system user."
                    ${USERMOD} -L ${_uname} >/dev/null 2>&1
                    if [ $? -ne 0 ]; then
                        echo "WARNING: unable to lock password for ${_uname} system account"
                    fi
                    ;;
                9)
                    ${USERMOD} -c "${descr}" -d "${homedir}" \
                        -s "${shell}" -g "${pgroup:-${_uname}}" \
                        ${user_groups} ${_uname} >/dev/null 2>&1
                    if [ $? -eq 0 ]; then
                        echo "Updated ${_uname} system user."
                    else
                        echo "WARNING: unable to modify ${_uname} system account"
                        echo "Please verify that account is compatible with these settings:"
                        show_acct_details "${_pretty_uname}" \
                            "${descr}" "${homedir}" "${shell}" "${groups}"
                        continue
                    fi
                    ;;
                *)
                    echo "ERROR: failed to create system user ${_pretty_uname}!"
                    return 1
                    ;;
            esac
        done
        return 0
    }
    _system_accounts_invoke 'chrony' '4.4' || exit $?    
  post-deinstall: |
    #!/bin/sh
    
    _chrony_homedir=/var/lib/chrony
    system_users=_chrony
    
    _system_accounts_invoke() {
    
        local USERMOD
    
        [ -z "$system_users" ] && return 0
    
        if command -v usermod >/dev/null 2>&1; then
            USERMOD="usermod"
        fi
    
        for acct in ${system_users}; do
            _uname="${acct%:*}"
    
            comment="$( (getent passwd "${_uname}" | cut -d: -f5 | head -n1) 2>/dev/null )"
            comment="${comment:-user} - removed package ${1}"
    
            if [ -z "$USERMOD" ]; then
                echo "WARNING: cannot disable ${_uname} system user (missing usermod)"
                continue
            fi
    
            ${USERMOD} -L -d /var/empty -s /usr/bin/false \
                -c "${comment}" ${_uname} >/dev/null 2>&1
            if [ $? -eq 0 ]; then
                echo "Disabled ${_uname} system user."
            fi
        done
        return 0
    }
    _system_accounts_invoke 'chrony' '4.4' || exit $?    
  pre-upgrade: |
    #!/bin/sh
    
    _chrony_homedir=/var/lib/chrony
    system_users=_chrony
    
    _system_accounts_invoke() {
    
        local USERADD USERMOD
    
        [ -z "$system_users" -a -z "$system_groups" ] && return 0
    
        if command -v useradd >/dev/null 2>&1; then
            USERADD="useradd"
        fi
    
        if command -v usermod >/dev/null 2>&1; then
            USERMOD="usermod"
        fi
    
        show_acct_details() {
            echo "   Account: $1"
            echo "   Description: '$2'"
            echo "   Homedir: '$3'"
            echo "   Shell: '$4'"
            [ -n "$5" ] && echo "   Additional groups: '$5'"
        }
    
        group_add() {
            local _pretty_grname _grname _gid
    
            if ! command -v groupadd >/dev/null 2>&1; then
                echo "WARNING: cannot create $1 system group (missing groupadd)"
                echo "The following group must be created manually: $1"
                return 0
            fi
    
            _grname="${1%:*}"
            _gid="${1##*:}"
    
            [ "${_grname}" = "${_gid}" ] && _gid=
    
            _pretty_grname="${_grname}${_gid:+ (gid: ${_gid})}"
    
            groupadd -r ${_grname} ${_gid:+-g ${_gid}} >/dev/null 2>&1
    
            case $? in
                0) echo "Created ${_pretty_grname} system group." ;;
                9) ;;
                *) echo "ERROR: failed to create system group ${_pretty_grname}!"; return 1;;
            esac
    
            return 0
        }
    
        # System groups required by a package.
        for grp in ${system_groups}; do
            group_add $grp || return 1
        done
    
        # System user/group required by a package.
        for acct in ${system_users}; do
            _uname="${acct%:*}"
            _uid="${acct##*:}"
    
            [ "${_uname}" = "${_uid}" ] && _uid=
    
            eval homedir="\$${_uname}_homedir"
            eval shell="\$${_uname}_shell"
            eval descr="\$${_uname}_descr"
            eval groups="\$${_uname}_groups"
            eval pgroup="\$${_uname}_pgroup"
    
            [ -z "$homedir" ] && homedir="/var/empty"
            [ -z "$shell" ] && shell="/usr/bin/nologin"
            [ -z "$descr" ] && descr="${_uname} user"
            [ -n "$groups" ] && user_groups="-G $groups"
    
            if [ -n "${_uid}" ]; then
                use_id="-u ${_uid} -g ${pgroup:-${_uid}}"
                _pretty_uname="${_uname} (uid: ${_uid})"
            else
                use_id="-g ${pgroup:-${_uname}}"
                _pretty_uname="${_uname}"
            fi
    
            if [ -z "$USERADD" -o -z "$USERMOD" ]; then
                echo "WARNING: cannot create ${_uname} system account (missing useradd or usermod)"
                echo "The following system account must be created:"
                show_acct_details "${_pretty_uname}" "${descr}" "${homedir}" "${shell}" "${groups}"
                continue
            fi
    
            group_add ${pgroup:-${acct}} || return 1
    
            ${USERADD} -c "${descr}" -d "${homedir}" \
                ${use_id} ${pgroup:+-N} -s "${shell}" \
                ${user_groups} -r ${_uname} >/dev/null 2>&1
    
            case $? in
                0)
                    echo "Created ${_pretty_uname} system user."
                    ${USERMOD} -L ${_uname} >/dev/null 2>&1
                    if [ $? -ne 0 ]; then
                        echo "WARNING: unable to lock password for ${_uname} system account"
                    fi
                    ;;
                9)
                    ${USERMOD} -c "${descr}" -d "${homedir}" \
                        -s "${shell}" -g "${pgroup:-${_uname}}" \
                        ${user_groups} ${_uname} >/dev/null 2>&1
                    if [ $? -eq 0 ]; then
                        echo "Updated ${_uname} system user."
                    else
                        echo "WARNING: unable to modify ${_uname} system account"
                        echo "Please verify that account is compatible with these settings:"
                        show_acct_details "${_pretty_uname}" \
                            "${descr}" "${homedir}" "${shell}" "${groups}"
                        continue
                    fi
                    ;;
                *)
                    echo "ERROR: failed to create system user ${_pretty_uname}!"
                    return 1
                    ;;
            esac
        done
        return 0
    }
    _system_accounts_invoke 'chrony' '4.4' || exit $?    

As you can see this not super pretty and the "pre-install" & "pre-upgrade" scripts are duplicated.

Enter systemd-sysusers. Now the chrony package includes a file sysusers.conf, which is installed into /usr/lib/sysusers.d/chrony.conf when the package is installed:

# Create chrony system user

u _chrony - "chrony user" /var/lib/chrony /usr/bin/nologin

When building the package cbuild detects the presence of a file installed into usr/lib/sysusers.d and adds a runtime dependency on the systemd-utils package, which contains the systemd-sysusers binary.

In turn, the systemd-utils package contains a trigger. Triggers are a concept built into the apk package manager. A package can have one trigger script that is run whenever a package changes the contents of a "monitored" directory. In this case the systemd-utils trigger is run whenever /usr/lib/syusers.d or /usr/lib/tmpfiles.d changes (systemd-tmpfiles is a story for another day).

As far as the systemd-sysusers part of the trigger script is concerned it runs /usr/bin/systemd-sysusers, which uses the declarative contents of /usr/lib/syusers.d to determine what system users and groups should exist and be active, then makes changes as needed.

This is the adbdump of systemd-utils-254-r5.44c71395.apk showing the trigger script and its monitored directories:

scripts:
  trigger: |
    #!/bin/sh

    # package script
    set -e

    # invoking sysusers is always harmless
    /usr/bin/systemd-sysusers || :

    # always create/remove/set
    TMPFILES_ARGS="--create --remove"

    # a little heuristical but unassuming with userland
    # the idea is that if /run is mounted, it's probably a running system
    # (doesn't matter if container or real) and has pseudo-filesystems
    # in place, otherwise we avoid messing with them
    if [ ! -r /proc/self/mounts -o ! -x /usr/bin/awk ]; then
        # bare system, don't mess with pseudofs
        TMPFILES_ARGS="$TMPFILES_ARGS -E"
    else
        RUN_FSTYPE=$(/usr/bin/awk '{if ($2 == "/run") print $1;}' /proc/self/mounts)
        RUN_FSTYPE=$(/usr/bin/awk '{if ($2 == "/run") print $1;}' /proc/self/mounts)
        if [ "$RUN_FSTYPE" != "tmpfs" ]; then
            # /run is not mounted or is something bad, don't mess with pseudofs
            TMPFILES_ARGS="$TMPFILES_ARGS -E"
        fi
    fi

    /usr/bin/systemd-tmpfiles $TMPFILES_ARGS || :    
triggers: # 2 items
  - /usr/lib/syusers.d
  - /usr/lib/tmpfiles.d

As you can see individual packages are now much simpler and all the complexity of managing the system users and groups is delegated to systemd-sysusers.

An added benefit of the removal of hook scripts is package actions (such as add/delete) become more atomic. For example if a "pre-install" hook script is run and makes changes, then the install step fails the changes made by the script will not be rolled back. In contrast trigger scripts are only run after the package action has successfully been committed. If something fails during the package action the transaction will be rolled back before any scripts are run.

Wrap Up

I found learning about this change valuable for better understanding how a hidden aspect of package management works, I hope you did too. If you'd like to see more posts like this feel free to let me know.