登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

systemd ProtectSystem 与 ReadWritePaths 怎么组合

来源:17golang原创

时间:2026-10-04 11:00:10 485浏览 收藏

ProtectSystem= 与 ReadWritePaths= 的正确组合思路是:先把服务看到的文件系统设成默认只读,再只为业务确实需要的目录恢复写权限。最常用的加固基线是 ProtectSystem=strict,配合少量绝对路径的 ReadWritePaths=;若路径属于状态、日志、缓存或运行时数据,优先改用 StateDirectory=、LogsDirectory=、CacheDirectory=、RuntimeDirectory=。

不要把 ReadWritePaths=/var 当成省事写法。它会让过大的目录重新可写,削弱 strict 的价值。迁移时应从服务真实写入清单出发,尽量把可写范围收敛到单个应用目录。

systemd 官方参考地址:https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html

先看清 ProtectSystem 的三档范围

ProtectSystem= 通过文件系统命名空间改变服务进程看到的挂载属性,不会把主机文件系统永久改成只读。三档配置的覆盖范围不同:

配置服务内变为只读的主要范围适合场景
ProtectSystem=yes/usr 以及引导目录 /boot、/efi低风险起步,不阻止服务修改 /etc 和大部分本地数据
ProtectSystem=full在 yes 的基础上增加 /etc服务只读配置但仍需写入其他业务目录
ProtectSystem=strict几乎整个文件系统层级;/dev、/proc、/sys 作为 API 文件系统子树单独处理默认拒绝写入,再显式放行少量目录

systemd ProtectSystem 三档只读范围

图1:ProtectSystem 三档只读范围,strict 建立最完整的默认只读基线。

strict 并不等于完整沙箱。它主要约束文件系统写入,不能代替普通 Unix 权限、SELinux 或 AppArmor,也不阻止程序连接只读目录中的 Unix socket。若服务仍拥有重新挂载所需的特权,部分限制还可能被撤销,因此高权限服务还应收紧 CAP_SYS_ADMIN 或挂载相关系统调用。

迁移前先列出真实写入点

很多服务启动失败,不是 ProtectSystem=strict 本身有问题,而是旧配置隐含了散落写入:程序目录旁的临时文件、/etc 下的自动生成配置、任意位置的 PID 文件、日志和缓存。迁移前把写入点按用途分组:

  • 持久状态:数据库、队列、索引,通常放到 /var/lib/myapp。
  • 日志:若程序自己写日志文件,通常放到 /var/log/myapp;若写 journal,则未必需要日志目录。
  • 缓存:可重建内容放到 /var/cache/myapp。
  • 运行时数据:socket、PID、临时状态放到 /run/myapp。
  • 配置:通常应只读;如果程序会自动改配置,应重新评估设计,而不是直接放行整个 /etc。

旧服务可能只有以下宽松配置。它依赖用户和文件权限,但没有为服务建立只读文件系统视图:

[Service]
# 旧配置只指定运行用户,文件系统仍有大量潜在写入面
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myapp

新写法:默认只读,只开放可写岛

如果目录已经由软件包或部署工具创建,可以直接用 ReadWritePaths= 放行。路径必须是绝对路径;一个指令可列出多个路径,也可以重复出现。

[Service]
# 先把服务视角下的整个文件系统设为默认只读
ProtectSystem=strict

# 只恢复应用状态和自有日志目录的写权限
ReadWritePaths=/var/lib/myapp /var/log/myapp

# 前缀 - 表示该路径不存在时不让服务启动失败
ReadWritePaths=-/run/myapp

ReadWritePaths= 只改变服务命名空间中的挂载可写性,不会创建目录,也不会绕过文件所有者、组、模式位、ACL 或强制访问控制。目录存在但归 root 所有时,以 User=myapp 运行的进程仍可能收到 Permission denied。

ProtectSystem strict 中的可写岛

图2:默认只读文件系统中的可写岛,放行路径仍受 Unix 权限和底层挂载状态约束。

若这些目录完全属于当前服务,更推荐由 systemd 创建和维护。下面的配置会为服务准备对应用途的目录,并让它们从 ProtectSystem=strict 的只读效果中排除:

[Service]
# 以严格只读作为文件系统基线
ProtectSystem=strict

# 由 systemd 创建目录并按服务身份管理所有权
StateDirectory=myapp
LogsDirectory=myapp
CacheDirectory=myapp
RuntimeDirectory=myapp

# 服务只写上述托管目录,不再单独放行整个 /var
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myapp

对应的常见路径分别是 /var/lib/myapp、/var/log/myapp、/var/cache/myapp 和 /run/myapp。这种写法把目录创建、所有权和生命周期放回 unit 配置,更适合新服务。只有现有目录布局无法调整,或必须写入某个外部挂载点时,再使用 ReadWritePaths=。

ReadWritePaths 不能覆盖哪些限制

组合配置时最容易误判的是“写入放行”并不等于“保证可写”。至少还有四层约束:

  1. 底层超级块只读:如果文件系统本身以只读方式挂载,ReadWritePaths= 不能把它变成可写。
  2. Unix 权限:服务用户仍要拥有目标目录的写权限。放行挂载属性不会自动执行 chown。
  3. 其他沙箱选项:InaccessiblePaths=、ProtectHome=、绑定挂载等设置可能对同一路径施加更严格限制。
  4. 命名空间能力:相关限制依赖文件系统命名空间支持;某些容器或用户级服务环境可能无法按系统服务的方式生效。

特别注意:不要先用 InaccessiblePaths=/data 隐藏整棵树,再期望用 ReadWritePaths=/data/myapp 恢复其中子目录。不可访问映射比只读更严格,不能靠内部可写路径重新打开。需要“隐藏大部分、显示少量”时,应重新设计目录或使用更适合的绑定挂载方案。

用 drop-in 完成低风险迁移

不要直接改发行版提供的 unit 文件。使用 drop-in 可以保留升级兼容性,也便于快速回滚。

# 创建或编辑服务的本地覆盖配置
sudo systemctl edit myapp.service

# 检查合并后的 unit 内容,确认没有被其他 drop-in 覆盖
systemctl cat myapp.service

先加入 ProtectSystem=full 观察服务是否还在修改 /etc,再迁移到 strict 只是可选的运维策略,不是必须步骤。如果已经有完整的写入清单,可以直接启用 strict。关键是每次只开放已确认的业务目录,不要在失败后不断扩大白名单。

修改后先做 unit 语法检查,再重载和重启:

# 验证 unit 文件语法与引用关系,不启动服务
sudo systemd-analyze verify /etc/systemd/system/myapp.service

# 让 PID 1 重新读取配置并重启目标服务
sudo systemctl daemon-reload
sudo systemctl restart myapp.service

# 查看启动失败原因和本次启动日志
sudo systemctl status myapp.service --no-pager
sudo journalctl -u myapp.service -b --no-pager

如果主 unit 位于发行版目录,systemd-analyze verify 也可以指向完整 unit 文件,随后通过 systemctl cat 检查实际合并结果。验证命令能发现语法和依赖问题,但不会替代真实写入测试。

同时做正向和负向写入测试

只确认服务能启动不够。至少要验证“允许写的目录能写”和“未允许的目录确实不能写”。可以用临时 unit 先验证系统对这组属性的支持:

# 准备允许写入的测试目录,普通权限仍需正确
sudo install -d -m 0755 /tmp/myapp-write

# /etc 写入应失败,而白名单目录写入应成功
sudo systemd-run --wait --pipe \
  -p ProtectSystem=strict \
  -p ReadWritePaths=/tmp/myapp-write \
  /bin/sh -c 'touch /etc/myapp-should-fail; touch /tmp/myapp-write/myapp-should-pass'

对正式服务,最好让应用自己的健康检查覆盖数据库落盘、日志轮转、socket 重建和升级后的首次写入。还要确认它不能修改可执行文件、系统配置和其他服务的数据目录。systemd-analyze security myapp.service 可以帮助观察整体暴露面,但评分不是功能测试,也不是安全保证。

迁移清单

  • 服务写入点是否已经按状态、日志、缓存和运行时数据分类?
  • 是否优先使用 StateDirectory= 等托管目录,而不是放行整个 /var?
  • 现有路径是否存在,服务用户是否确实拥有普通写权限?
  • 目标文件系统的底层挂载是否可写?
  • ProtectHome=、InaccessiblePaths= 等设置是否与放行路径冲突?
  • 是否完成 unit 语法检查、启动测试、允许路径写入测试和禁止路径负向测试?
  • 服务是否仍保留能够撤销挂载限制的高权限能力?
  • 回滚时是否只需删除 drop-in 并执行 daemon-reload 与 restart?

最终判断标准很简单:服务的正常数据、日志和运行时文件都能写入明确的自有目录;除此之外的系统路径在服务命名空间中保持只读;即使某个业务进程被利用,攻击者也不能轻易把改动扩散到系统配置、程序文件或其他服务的数据。

常见问题

ReadWritePaths 会自动创建目录吗?

不会。目录可能不存在时可用 - 前缀忽略缺失,但真正需要长期使用的目录更适合用 StateDirectory=、LogsDirectory= 等指令创建。

配置 ReadWritePaths 后为什么仍然 Permission denied?

先检查底层文件系统是否只读,再检查服务用户、目录所有权、模式位、ACL、SELinux 或 AppArmor。该指令只恢复命名空间中的可写挂载属性,不会绕过这些权限。

ProtectSystem=strict 后 /tmp 一定只读吗?

通常 strict 会覆盖整个层级,但若同时启用 PrivateTmp=,服务会得到独立且可写的 /tmp 和 /var/tmp。因此要根据完整 unit 配置判断,而不是只看一行。

用户级 service 能直接使用这些配置吗?

依赖文件系统命名空间的沙箱功能在用户管理器中通常受限;在支持非特权用户命名空间且配合 PrivateUsers=true 时,部分设置才可能工作。生产加固前应在目标发行版和运行环境中实测。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>