服务包经常需要先准备专用账号,才能拥有文件或避免以 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。发行版启动单元和包集成决定工具何时被调用,因此需要验证实际安装路径及服务依赖,避免守护进程在账号或状态目录准备完成之前启动。账号创建与目录准备需要协调,但两者仍是不同操作,失败语义和验证方式也不相同。
只需要主机本地身份时,可以使用自动分配。如果共享存储或已经公开的接口要求某个数字身份,就应明确建立并验证分配规则,包括数字冲突与既有账号的处理。安装后还要测试实际服务访问。名称稳定、数字稳定和文件所有权正确是相关要求,但没有哪一项能够自动替代其余证据。
延伸阅读
来源
- systemd 252 sysusers.d,用户与组指令、分配区间、文件优先级和家目录字段,已核验 2026-09-12。
- systemd 252 systemd-sysusers,账号创建、重复执行与替代根目录处理,已核验 2026-09-12。
- systemd 252 tmpfiles.d,独立创建服务目录与设置所有权,已核验 2026-09-12。