Linux 严格只读隔离后配置写不进去:ReadWritePaths、RuntimeDirectory 与回滚检查
来源:17golang原创
时间:2026-08-20 23:55:36 185浏览 收藏
服务升级后突然报 Read-only file system,很多人第一反应是去改目录属主,跑chmod调权限。如果服务的unit配置里新增了 ProtectSystem=strict,出问题的根源大概率是服务进程感知到的挂载视图变了:哪怕宿主机上对应目录权限已经设成755,进程所在的隔离命名空间也会把这部分路径识别成只读状态。
排查这类问题的思路很清晰,先定位哪部分路径被挂载成了只读,再判断是把写入操作迁移到专属运行目录,还是只为确实需要持久化的存储路径开一个最小范围的可写权限口子。
启用ProtectSystem=strict之后出现写入失败,先别反复改宿主机文件权限,优先核对systemd挂载隔离层的路径规则,再按临时运行数据、持久业务数据的属性拆分可写范围,最后做全链路回滚校验。
ProtectSystem=strict限制的是服务进程视角下的文件系统视图,和普通的文件属主权限控制不是同一层逻辑。- 临时PID文件、socket套接字和缓存数据优先放进
RuntimeDirectory;需要持久化留存的业务数据,才考虑配置ReadWritePaths=。 - 修改unit配置后必须执行daemon-reload、重启服务,并用
systemctl show与findmnt校验新启动的进程实际受的权限边界。 - 操作回滚时要先撤掉新增的隔离规则,再恢复原来的目录写入路径,避免配置改一半留下隐蔽的故障点。
升级后写入失败,先区分普通权限错误和挂载只读
假设服务名是 report-worker.service,升级后它需要把临时状态写到 /var/lib/report-worker,服务日志里却抛出了下面的报错:
open /var/lib/report-worker/state.json: Read-only file system
这个报错和 Permission denied 属于完全不同的问题场景。前者代表内核直接拒绝了写入操作的文件系统属性限制,后者才更可能是UID、GID或者文件权限位配置不对。排查顺序应该先确认unit里有没有启用这类隔离项,再查服务进程实际的挂载信息:
systemctl cat report-worker.service systemctl show report-worker.service -p ProtectSystem -p ReadWritePaths -p RuntimeDirectory pid=$(systemctl show -p MainPID --value report-worker.service) findmnt -no TARGET,OPTIONS --target /var/lib/report-worker test "$pid" -gt 0 && grep ' /var/lib/report-worker ' /proc/$pid/mountinfo
最后一条命令只能在服务已经生成主进程的情况下执行。如果unit是一次性任务或者正处于重启流程,要先读取 systemctl status 的运行状态,再获取对应PID,千万不能把旧PID的挂载检测结果当成当前新版本的运行依据。

ProtectSystem=strict 到底改变了哪些路径规则
ProtectSystem=strict 会让服务进程看到的系统级目录全部进入只读或者不可写入的隔离视图。它的设计目标是降低长期运行的后台服务随意修改系统文件的风险,并不是用来替代文件属主、ACL或者SELinux策略的。这里要特别注意:哪怕你在宿主机上用root账号写入同一个目录完全正常,也不代表unit配置里运行的服务进程能拿到同等的写入权限视图。
| 需求场景 | 推荐配置方案 | 验收核心要点 |
|---|---|---|
| PID文件、socket、短期缓存 | RuntimeDirectory=report-worker | /run/report-worker 随服务启动自动创建 |
| 必须持久化的业务状态 | ReadWritePaths=/var/lib/report-worker | 只开放目标文件夹,不要放开整个 /var 目录的可写权限 |
| 静态配置和程序二进制文件 | 保持默认只读状态 | 服务启动后不能自行改写安装目录下的任何文件 |
如果你的应用现在把临时文件和长期业务状态混存在同一个目录里,优先拆分不同用途的目录,远比直接加一条宽泛的可写规则稳妥得多。隔离规则开得越松,后续排查问题的时候,就越难判断某次意外写入是不是真的有业务逻辑支撑。
优先把运行态数据迁移到 RuntimeDirectory
运行态文件本来就不应该依赖运维手工提前创建的 /var/run 子目录。你可以直接修改unit配置,让systemd在服务启动时自动生成对应运行目录,再通过环境变量把这个路径传递给业务程序:
[Service] ProtectSystem=strict RuntimeDirectory=report-worker RuntimeDirectoryMode=0750 Environment=REPORT_RUNTIME_DIR=/run/report-worker 启动指令=/usr/local/bin/report-worker
程序侧把PID、socket或者可丢弃的临时缓存写到 REPORT_RUNTIME_DIR 路径下,需要跨重启保留的数据再放到单独的持久存储目录里。这样服务停止之后,运行目录的清理行为是完全可预期的,不会出现把临时生成的日志文件误当成核心业务数据留存的问题。
如果你的程序写死了固定路径,暂时没法改代码适配新的运行目录规则,才考虑用 ReadWritePaths 做临时过渡。就算是过渡配置,也要在注释里写清对应目录的用途,后续再安排把代码逻辑切到标准运行目录的路径下:
[Service] ProtectSystem=strict ReadWritePaths=/var/lib/report-worker RuntimeDirectory=report-worker
ReadWritePaths 只开最小权限口子,别用整棵目录兜底
ReadWritePaths= 的核心价值是明确声明“这个服务业务逻辑上确实需要写入这个位置”,而不是直接把全局的只读保护全部关掉。别因为某个状态文件写不进去,就直接改成 ProtectSystem=no 或者开放 /var 全路径;这种操作会让原本能被隔离规则拦截到的路径误写问题,重新变成很难发现的静默风险。
配置改完之后,先让systemd重新读取全部unit配置,再重启服务生成新的进程:
systemctl daemon-reload systemctl restart report-worker.service systemctl show report-worker.service -p ProtectSystem -p ReadWritePaths -p RuntimeDirectory systemctl status report-worker.service --no-pager
修改完unit配置却不执行 daemon-reload,或者只重载配置却不重启服务,都会造成“配置文件看起来已经改对了,实际运行现场完全没变化”的假象。验收的时候最好把unit文本、属性输出结果和新生成的主PID一起留存,后续排查故障的时候就能明确对应到到底哪一版规则在生效。

回归检查要覆盖正常写入、重启和异常失败路径
只看到服务运行状态变成 active (running) 完全不够,至少要做一遍正常写入验证、重启后状态复查,还有越界写入拦截测试:
- 写入
/run/report-worker的PID或者socket文件成功,服务日志里没有出现只读相关的报错。 - 重启服务之后,运行态文件按预期重新生成;需要持久化保留的状态数据仍然完整保存在
/var/lib/report-worker路径下。 - 尝试往系统目录
/usr/local或者没有列入ReadWritePaths规则的路径写入数据,服务应该直接报错退出,并且留下明确可识别的错误日志。 - 用新生成的
MainPID复查/proc/$pid/mountinfo挂载视图,确认检测结果对应的是新进程的状态,不是旧进程的残留数据。 - 回滚操作前先备份当前的unit文件和服务状态,再删掉本次新增的隔离配置项,执行
daemon-reload与重启,最后重新校验原来的写入路径是否正常。不要只删掉ProtectSystem规则,却保留程序已经切换过去的新环境变量,不然会把原本的权限问题变成更难排查的“路径不存在”故障。
常见问题
ProtectSystem=strict 会影响配置文件的读取操作吗?
它主要限制写入行为;读取操作是否能正常执行,还要结合ProtectHome、InaccessiblePaths、文件本身权限和实际配置的挂载边界一起判断,不能只凭这一个参数的名字就推断全部的访问结果。
ReadWritePaths 能绕过Linux原生的文件权限规则吗?
不能。它只是调整服务进程视角下的文件系统可写范围,原本的UID、GID、ACL和其他安全策略仍然会正常生效。
RuntimeDirectory 里的文件重启后还会保留吗?
它本身就是为运行态临时数据设计的,生命周期和服务绑定;需要跨重启留存的内容要放到单独的持久目录里,再明确通过ReadWritePaths规则开放这个目录的可写权限。
为什么改完unit配置之后路径还是只读状态?
最常见的几个原因是没有执行daemon-reload重载配置、服务本身没有重启、检查权限的时候取了旧PID的信息,或者有drop-in配置文件覆盖了主unit里的规则。你可以用systemctl cat、systemctl show和新的MainPID三组信息交叉核对,很快就能定位到问题。
把隔离配置变成可回滚的迁移项
这次配置调整真正要确认的不是“服务能不能正常启动”,而是每一类文件都有明确的归属路径:运行态数据放到RuntimeDirectory,持久化业务状态只开放必要的目录,系统级文件全程保持只读。完成daemon-reload、重启、正常写入验证、越界拦截测试和回滚复测之后,ProtectSystem=strict才算从一行写在配置里的参数,变成真正可落地校验的生产级安全约束。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
文章 · linux | 4小时前 | Linux · psi · 服务治理 · system · 内存压力 · Linux PSI systemd-oomd ManagedOOMMemoryPressure oomctl452 收藏
-
119 收藏
-
416 收藏
-
273 收藏
-
文章 · linux | 2天前 | Linux · 系统调用 · 故障排查 · 文件安全 · 路径解析 · Linux 路径解析 openat2 RESOLVE_BENEATH RESOLVE_IN_ROOT356 收藏
-
文章 · linux | 2天前 | Linux · 热更新 · 文件描述符 · io_uring · 并发排空 · Linux io_uring 固定文件热更新 draining IORING_REGISTER_FILES_UPDATE226 收藏
-
文章 · linux | 2天前 | Linux · 故障排查 · 文件描述符 · io_uring · 并发更新 · Linux io_uring IOSQE_FIXED_FILE EBADF 固定文件102 收藏
-
293 收藏
-
文章 · linux | 2天前 | Linux · 故障排查 · 文件系统 · inotify · 事件队列 · Linux inotify IN_Q_OVERFLOW max_queued_events 文件变更监听428 收藏
-
473 收藏
-
416 收藏
-
453 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习