systemd ProtectSystem 与 ReadWritePaths 怎么组合
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 文件系统子树单独处理 | 默认拒绝写入,再显式放行少量目录 |

图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。

图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 不能覆盖哪些限制
组合配置时最容易误判的是“写入放行”并不等于“保证可写”。至少还有四层约束:
- 底层超级块只读:如果文件系统本身以只读方式挂载,
ReadWritePaths=不能把它变成可写。 - Unix 权限:服务用户仍要拥有目标目录的写权限。放行挂载属性不会自动执行
chown。 - 其他沙箱选项:
InaccessiblePaths=、ProtectHome=、绑定挂载等设置可能对同一路径施加更严格限制。 - 命名空间能力:相关限制依赖文件系统命名空间支持;某些容器或用户级服务环境可能无法按系统服务的方式生效。
特别注意:不要先用 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 时,部分设置才可能工作。生产加固前应在目标发行版和运行环境中实测。
Python sqlite3.Connection.blobopen 怎么增量读写大字段
- 上一篇
- Python sqlite3.Connection.blobopen 怎么增量读写大字段
- 下一篇
- kazumi开源项目怎么参与?主仓库、规则仓库与版本支持说明
-
- 文章 · linux | 4小时前 | Linux · Linux systemd-journald journald.conf RateLimitIntervalSec RateLimitBurst 日志限速
- systemd-journald 日志限速丢弃怎么调整
- 208浏览 收藏
-
- 文章 · linux | 6小时前 |
- journalctl 怎么按服务某次 invocation 筛选日志
- 466浏览 收藏
-
- 文章 · linux | 8小时前 | Linux · systemd Restart RestartMode 依赖单元
- systemd RestartMode 怎么减少依赖单元连锁失败
- 239浏览 收藏
-
- 文章 · linux | 11小时前 | 定时任务 · Linux · 运维 · Cron OnCalendar Persistent systemd timer systemd-analyze calendar
- systemd timer 替代 cron 的日历表达式配置
- 364浏览 收藏
-
- 文章 · linux | 15小时前 | Linux · 内存管理 · Linux 内存限制 cgroup v2 memory.events
- cgroup v2 限制服务内存并观察回收事件
- 494浏览 收藏
-
- 文章 · linux | 1天前 | Linux · mount namespace unshare Linux挂载隔离
- mount namespace 隔离临时挂载的操作边界
- 462浏览 收藏
-
- 文章 · linux | 2天前 | Linux · 运维 · GNU tar 增量归档 listed-incremental exclude-from CACHEDIR.TAG
- tar 增量归档排除缓存目录的参数组合
- 324浏览 收藏
-
- 文章 · linux | 5天前 | Linux · 运维 · 日志排查 · journalctl 启动日志 Boot ID --list-boots systemd日志导出
- journalctl 按启动会话筛选并导出日志
- 369浏览 收藏
-
- 文章 · linux | 5天前 | Linux · Linux防火墙 nftables verdict map 多端口策略
- nftables verdict map 组织多端口策略的规则设计
- 475浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 325次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 382次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 376次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 342次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 167次使用
-
- Go os.File.Chmod 在 Unix 与 Windows 上的差异:权限位写入何时不会按预期生效
- 2026-08-28 427浏览
-
- Go os.OpenFile 权限参数在 Windows 上为什么表现不同
- 2026-09-11 308浏览
-
- Go 文件权限 0644 在 Windows 上为什么没有同样效果
- 2026-09-09 137浏览
-
- 详解CentOS7下PHP+Nginx+Mysql编译安装方法
- 2023-01-21 432浏览
-
- Linux 服务单元怎么加固:只读根、私有临时与可写路径白名单
- 2026-07-18 259浏览

