本文简介部分译自 0pointer.net.

简介

TL;DR: systemd 现在支持在服务启动时动态分配一个 Unix 用户 ID 给服务进程, 并在它退出时释放用户. 它安全, 支持短期, socket 激活和模板服务.

systemd 235 的发布带来其他改进的同时拓展了动态用户的逻辑. 动态用户是一个强有力但并不被广泛了解的概念, 自 systemd 232 开始具有基础的支持. 这篇博客希望能让它能被更多人所认知.

UNIX 用户的概念是一个最基础于最被广泛了解的 POSIX 操作系统安全概念. 他是 UNIX/POSIX 最主要的安全概念, 被每个人认同, 且几乎所有后来的安全概念 (CAP, SELinux, 其他 MACs, 用户 name-spaces…) 都在某种程度基于它而构建, 并将它拓展, 或是至少与它交互. 如果阁下构建一个关闭所有安全功能的 Linux 内核, UNIX 用户概念基本是阁下所拥有的唯一安全功能.

系统用户

起初, 用户概念的引入被用作使所用户系统更加稳定, 也就是多个 人类 用户同时共享同一个系统, 将他们的资源干净地分离并充当他们之间的保护层. 不过, 现在大部分的 UNIX 系统并不真正需要那样的多用户概念.Service 如今, 更多的系统可能只有一个 (或更少) 人类用户, 但他们的 /etc/passwd 也许有更多用户的存在. 大部分 UNIX 用户是 系统用户, 即用户在技术上不再代表坐在电脑前的人类, 而是运行中进程的一个安全身份. 即使传统的, 并行的多用户系统变得不那么流行, 它们引入的开创性概念如今也变成了 UNIX 系统安全的基石. 现代系统转向了隔离的服务 – 每个服务以它自己的系统用户身份运行, 因此, 运行在一个最小化的安全环境中.

对比 Android

Android 系统背后的开发者意识到了 UNIX 用户概念与 UNIX 系统主要安全措施的关联, 将其进一步拓展: Android 上不仅系统服务使用 UNIX 多用户概念, 每个图形应用也是, 因此每个应用进程被分离并保护.

系统用户在 Linux 的限制

回到传统 Linux 世界, 这类概念并没有 Android 那么发展充分. 即使有着 UNIX 典型安全概念的多用户, 系统用户的分配和管理通常是十分受限的, 十分原始和死板. 在大多数情况下, 软件包管理器在安装时运行安装脚本来分配 (通常一个) 系统用户. 从那以后, 用户将永远留在系统上, 从不被删除, 即使这个软件包已经被移除. 大多数 Linux 发行版限制 1000 个系统用户 (并不多). 分配一个系统用户在这种情况下就显得十分昂贵了: 可用的用户数量受到限制, 且没有一个被定义的方法能在事后移除系统用户. 如果阁下太过自由地使用系统用户, 那么将很可能用完 1000 个系统用户的限制.

UID 循环

阁下可能会好奇: 为何系统用户在包卸载后通常不被删除呢?

理由藏在一个多用户概念的属性中 (阁下甚至可能称其为 设计失误): 用户 ID (UID) 和文件权限紧密相联 (以及其他东西, 例如 IPC). 如果一个以 Kimiblock114514 (UID 810) 的身份运行的服务在 /下北泽 创建了一个文件, 后被结束进程并移除用户, 这个文件依旧为数字 UID 810 所有. 当下一个系统用户碰巧被分配到相同的数字 UID 时 (由于对 ID 循环利用), 使用它运行的服务将会获得 /下北泽 的访问权限. 由于 /下北泽 属于另一个不相关的服务, 这引入了一个巨大的安全漏洞. 因此, 发行版倾向于避免 UID 循环 – 系统用户被分配后将永久保留在系统上.

动态用户

启用与实现

要启用动态用户, 只需在 service unit 中 [Service] 定义 DynamicUser=yes.

动态用户被启用后, 在服务启动的瞬间将会在 61184–65519 随机分配一个未被使用的 UID 给进程. 若 RuntimeDirectory, StateDirectory 等定义的路径不属于这个用户, systemd 将会自动 chmod() 这个目录. 但不必担心 – systemd 将会自动避免 UID 更变.

DynamicUser=yes 将默认启用如下保护:

  • ProtectSystem=strictProtectHome=read-only

    • 这意味着几乎整个系统目录对服务只读, 除去一些例外: /proc, /sys, /tmp/var/tmp 等.
  • PrivateTmp=yes

    • 程序将会得到私有的 /tmp/var/tmp, systemd 将其储存在 /var/tmp/systemd-private-*-${serviceName}/tmp 等私有位置, 但程序只会看到一个空的 /var/tmp, 指向私有地址. 这些 临时目录 由 root 拥有 – 不必担心其他程序查看或修改.
  • RemoveIPC=yes

    • 这意味着服务停止后, 所有 SysVPOSIX IPC object 将会被移除.

以上的保护措施确保了使用动态用户的服务被安全地置于沙盒内, 不能对系统目录做出修改, 停止时删除所有临时文件.

若是阁下有需求, RuntimeDirectory 可以被用来打通沙盒内与外部系统的连接. 它将会在 /run/${RuntimeDirectory} 设置一个目录, 用来放置 UNIX socket 等文件, 并在服务停止时删除.

持久性数据

当然, 运行于动态用户下的服务有一个很大的限制: 服务无法保存持久性数据, 重新启动后所有保存的数据都将被清除. systemd 235 引入了 3 个属性来移除这个限制: StateDirectory, LogsDirectoryCacheDirectory. 它们与 RuntimeDirectory 类似, 在 /var/lib /var/log/var/cache 下创建子目录. 在它们之间有一个主要的不同: 创建的目录将会是持久性的 – 它们不会在重启服务时消失, 因此适用于持久保存程度数据. 需要注意的是, /var/lib/${StateDirectory} 是指向 /var/lib/private/${StateDirectory} 的软链接.

StateDirectory 指向外部目录

StateDirectory 的引入极大地拓展了动态用户的适用范围, 但仅支持在 /var/lib 下储存数据. 因此, 需要引入另一项 systemd 功能: mount unit.

通过 mount unit, 阁下能将外部目录 bind mount 到 /var/lib/private/${StateDirectory} 以保存数据到 /var 外. mount unit 的命名需要使用 systemd-escape 获得:

1
systemd-escape --path /var/lib/private/${StateDirectory}

以下是 bind mount 的示例 var-lib-private-serverOS\\x2dPaper.mount:

1
2
3
4
[Mount]
What=/mnt/114514/Paper
Where=/var/lib/private/serverOS-Paper
Options=bind,noauto

bind mount 创建后, 还需要在 service 中 [Unit] 定义 RequiresMountsFor=/var/lib/private/${StateDirectory}.

示例

Matrix Sliding Sync

/usr/lib/systemd/system/serverOS-sliding-sync.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[Unit]
Description=serverOS Matrix Sliding Sync Proxy
After=serverOS-synapse.service

[Service]
Type=simple
DynamicUser=yes
ExecStart=/usr/lib/serverOS/serverOS-sliding-sync
Restart=always
RestartSec=60s
SyslogIdentifier=sliding-sync
EnvironmentFile=/usr/share/serverOS/sliding-sync/envs
RuntimeDirectory=serverOS-syncv3
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelLogs=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
ProtectClock=true
RestrictRealtime=true
PrivateDevices=true
PrivateTmp=true
ProtectHostname=true

[Install]
WantedBy=multi-user.target

AdGuard Home

/usr/lib/systemd/system/serverOS-AdGuardHome.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[Unit]
Description=DNS
PartOf=network-online.target
RequiresMountsFor=/var/lib/private/adguardhome
After=network.target
Before=NetworkManager-wait-online.service

[Service]
DynamicUser=yes
StartLimitInterval=5
StateDirectory=serverOS-AdGuardHome
WorkingDirectory=/var/lib/private/serverOS-AdGuardHome
ExecStartPre=cp /var/lib/adguardhome/AdGuardHome /var/lib/private/serverOS-AdGuardHome/aghexec
ExecStart=/var/lib/private/serverOS-AdGuardHome/aghexec "-s" "run"
Restart=always
RestartSec=5

OOMPolicy=stop
OOMScoreAdjust=-500

SyslogIdentifier=AdGuardHome
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelLogs=true
ProtectControlGroups=true
MemoryDenyWriteExecute=true
ProtectClock=true
RestrictRealtime=true
PrivateDevices=true
PrivateTmp=true
ProtectHostname=true
ProtectHome=true
ProtectSystem=strict

[Install]
WantedBy=multi-user.target

/usr/lib/systemd/system/var-lib-private-serverOS\\x2dAdGuardHome.mount

1
2
3
4
[Mount]
What=/mnt/114514/AdGuardHome
Where=/var/lib/private/serverOS-AdGuardHome
Options=bind,noauto