systemd-sysusers 创建缺少的服务账号
服务包经常需要先准备专用账号,才能拥有文件或避免以 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 账号。应用声明后,会生成工作用户、它的主组、读者组,以及工作用户的附加组成员关系。脚本检查数字主组关系和实际成员列表,而不是只检查配置能够解析。这样可以明确证明创建结果满足所声明的访问结构,避免把一个存在的组误认为用户已经加入其中。 ...