服务包经常需要先准备专用账号,才能拥有文件或避免以 root 身份运行。systemd-sysusers 用一份小配置声明账号及其组,核心操作是创建缺少的身份,并建立要求的成员关系。它不是通用账号迁移引擎,不负责创建所有目录,也不保证自动分配的数字标识在每台机器上都相同。把这些职责分清楚,才能判断声明已经执行究竟证明了什么。

坏掉的样子

u 条目创建缺少的系统用户,通常也创建同名主组;g 创建组,m 添加附加组关系。ID 写成 - 表示自动分配,r 条目可以限制分配池,但已有账号占用了某个数字,就会改变剩余可用范围。因此相同的分配池并不能建立跨机器身份一致性,尤其不能因为两边使用同一份配置,就假定共享存储里的数字所有者一定对应同一人。

更改既有账号的声明,不会自动为它重新编号,也不会自动更新家目录和描述字段。这种行为便于重复执行包初始化,但同时意味着配置差异不等于活动账号记录已经发生变化。真正迁移 UID,需要另外处理既有文件归属、正在运行的进程、共享存储和回退策略;不能只改一行声明,再把命令返回成功当作迁移完成。

家目录列设置的是账号数据库字段,不会创建实际目录。因此,一个服务可能已经拥有有效账号,却仍因为状态路径不存在或不可写而启动失败。需要目录时,应使用合适的受管理服务目录或 tmpfiles 规则,明确创建、所有权和权限。账号存在与目录可用,是两项独立条件,应沿真实服务启动路径分别验证。

快速验证

在装有 systemd-sysusers 的一次性 Linux 环境中,以管理员身份运行下面的 Python 脚本。它不修改机器正在使用的账号数据库,实测环境为 Debian 12、systemd 252.39。每组测试都通过 --root 指向单独的临时根目录;shadow 数据库只在内部比较内容,不会把值打印出来:

from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess

definition='''r - 61000-61010
u field-worker - "Field worker" /var/lib/field-worker /usr/sbin/nologin
g field-readers -
m field-worker field-readers
'''
def prepare(root,occupied=None):
    (root/'etc').mkdir(parents=True)
    passwd='root:x:0:0:root:/root:/bin/sh\n';group='root:x:0:\n'
    if occupied is not None:
        passwd+=f'occupied:x:{occupied}:{occupied}:Fixture:/nonexistent:/usr/sbin/nologin\n'
        group+=f'occupied:x:{occupied}:\n'
    (root/'etc/passwd').write_text(passwd);(root/'etc/group').write_text(group)
    directory=root/'usr/lib/sysusers.d';directory.mkdir(parents=True)
    (directory/'field.conf').write_text(definition)
def apply(root):
    result=subprocess.run(['systemd-sysusers','--root='+str(root)],capture_output=True,text=True,timeout=10)
    assert result.returncode==0,result.stderr
def entries(root,name):
    return {line.split(':')[0]:line.split(':') for line in (root/'etc'/name).read_text().splitlines()}
with TemporaryDirectory(prefix='xbcatm-sysusers-') as directory:
    first=Path(directory)/'first';second=Path(directory)/'second';prepare(first);apply(first)
    worker=entries(first,'passwd')['field-worker'];groups=entries(first,'group')
    assert worker[3]==groups['field-worker'][2]
    assert 'field-worker' in groups['field-readers'][3].split(',')
    assert worker[5:]==['/var/lib/field-worker','/usr/sbin/nologin']
    assert not (first/'var/lib/field-worker').exists()
    before={name:(first/'etc'/name).read_bytes() for name in ('passwd','group','shadow','gshadow')}
    apply(first)
    assert before=={name:(first/'etc'/name).read_bytes() for name in before}
    print('user, primary group, and supplementary membership created; repeat leaves databases unchanged')
    print('home and nologin shell recorded; home directory was not created')
    prepare(second,int(worker[2]));apply(second)
    other=entries(second,'passwd')['field-worker']
    assert other[2]!=worker[2]
    print('same allocation range and definition choose different UIDs when existing accounts differ')
    changed=definition.replace('u field-worker - "Field worker" /var/lib/field-worker',
                               'u field-worker 62000 "Changed description" /changed-home')
    (first/'usr/lib/sysusers.d/field.conf').write_text(changed);apply(first)
    assert entries(first,'passwd')['field-worker']==worker
    print('changing a declaration does not renumber or rewrite an existing account')
    assert not (first/'changed-home').exists()
print('only temporary alternate-root account databases were changed')

第一份根目录起初只有样本 root 账号。应用声明后,会生成工作用户、它的主组、读者组,以及工作用户的附加组成员关系。脚本检查数字主组关系和实际成员列表,而不是只检查配置能够解析。这样可以明确证明创建结果满足所声明的访问结构,避免把一个存在的组误认为用户已经加入其中。

用户记录包含要求的家目录路径和 nologin shell,但家目录本身并不存在。这直接区分了身份声明与文件系统准备。用于服务执行的账号仍然可以被获得授权的服务管理器使用;nologin shell 也不能替代所有权限和凭据控制。应根据程序真实的执行入口理解账号用途,而不是把账号字段当成完整隔离策略。

再次应用相同声明,passwd、group、shadow 和 gshadow 文件内容均不变。这验证的是当前明确输入的重复执行行为,不代表任何可能的声明都没有副作用。遇到输入冲突或分配范围耗尽时,仍应根据工具报告定位原因,不能因为它被称为声明式工具,就跳过对失败结果的检查。

第二份根目录先让另一个样本账号占用第一份工作用户的数字身份,再应用完全相同的配置,工作用户便得到不同 UID。名称与要求的分配区间相同,已有数据库状态却不同。因此,在两个根目录之间直接复制数字文件所有权、却不进行身份映射,是不成立的做法。是否要求跨主机数字一致,应当由数据共享契约决定。

最后,把第一份根目录中既有工作用户的声明改成新的数字 UID、描述和家目录,再次执行,完整账号记录仍然保持原样,也没有创建新目录。这体现的是以创建为主的初始化边界,不是证明新字段已经迁移成功。实验结束后删除临时根目录,所有账号变化始终局限于这些替代数据库。

明确打包时点与身份规则

软件包通常把声明放在 /usr/lib/sysusers.d,管理员可以在 /etc/sysusers.d 中用同名文件覆盖。文件名顺序与重复名称会影响最终输入,不能承诺声明永远与排列顺序无关。当多个包或本地策略涉及同一身份时,应检查真正生效的文件,以及冲突提示,而不是只阅读自己刚编辑的那份配置。

写入文件本身不会执行 sysusers。发行版启动单元和包集成决定工具何时被调用,因此需要验证实际安装路径及服务依赖,避免守护进程在账号或状态目录准备完成之前启动。账号创建与目录准备需要协调,但两者仍是不同操作,失败语义和验证方式也不相同。

只需要主机本地身份时,可以使用自动分配。如果共享存储或已经公开的接口要求某个数字身份,就应明确建立并验证分配规则,包括数字冲突与既有账号的处理。安装后还要测试实际服务访问。名称稳定、数字稳定和文件所有权正确是相关要求,但没有哪一项能够自动替代其余证据。

延伸阅读

来源