日志能追加却无法重建:检查目录的 ACL
向文件追加不需要在目录中新建名字,而创建文件需要。在放宽任何权限之前,先比较访问 ACL 与继承的默认值。
持续增长的日志会在某个东西重建它的那一刻停止增长,而错误信息说权限被拒绝。诱人的结论是日志文件本身需要更多权限,但该文件可能早已对同一账户可写。向已有名字追加与创建新名字是两项不同的检查,只有后者才会查询目录。
追加与创建是两项不同的检查
以追加方式打开已有文件,需要对那个文件的写权限,以及沿路径每一级目录的搜索权限。创建文件需要对包含它的目录有写和搜索权限,因为该操作会向目录中添加一个名字。通过重命名替换文件还会触及两个目录及其 sticky bit 规则。
| Operation | What must permit it | Typical failure message |
|---|---|---|
| Append to an existing log | File write access, path traversal | Permission denied on the file |
| Create a missing log | Directory write and search access | Permission denied on the path |
| Rename a replacement into place | Both directories, plus sticky restrictions | Permission denied or operation not permitted |
已经打开的描述符构成第四种情况。进程可以持有在放弃特权之前打开的写句柄,却仍无法以之后身份重新打开同一路径。成功追加与创建失败可以同时为真,对同一进程、在同一时刻。
默认 ACL 是被继承的,它们不授予权限
以默认前缀开头的条目描述新对象从目录继承什么。它不向目录本身授予任何权限,也不修复已存在文件的 ACL。
namei -l /var/log/openvpn/server.log
getfacl -p /var/log/openvpn
getfacl -p /var/log/openvpn/server.log
将访问条目与默认条目分开阅读,并阅读掩码。列出完整权限的具名条目仍可能被掩码限制,这就是有效注释比第一行字母更重要的原因。
使用解析后的路径,而不是配置中的路径。符号链接、挂载点或轮转过的目录可能把真实文件放在你授予的权限永远到不了的地方。先用名称工具走一遍路径,再把每一级与相关账户比较。猜测顶层目录,正是修复落点高了一级的原因。
确认那一刻是谁在写
在改动任何东西之前,检查服务单元、它的 drop-in 以及守护进程自身的账户设置。追加的进程与重建的进程可能不同:轮转助手、状态写入者和 Web 读取者很少共用同一身份。
从 root shell 跑一次测试并不能复现受限的服务,而且它会成功,服务却持续失败。在同一单元、同一账户和同一沙箱设置下复现失败的操作。如果做不到,用临时 shell 启动该单元,并检查它报告的身份。其他做法测的是你的终端,而不是服务。
如果确实需要创建,就给写入者访问专用日志目录的权限,并把使用方的读权限分开。为了让一次日志调用通过而把共享账户或整棵应用树设为可写,是拿小问题换大问题。
轮转时不要重新引入问题
重建文件的人决定下一个周期它的属主和模式,因此要把修复与轮转配置协调起来。在另一个账户下或用全新 ACL 创建的替代文件,会在修复成功后让故障回来。
在创建时设置模式和属组,而不是事后,并验证下一次轮转而不是下一次追加。追加只是重复已经通过的测试。如果轮转工具提供创建模式设置,就用它代替创建后的 chmod。创建与 chmod 之间的间隙,正是下一次写入可能失败的时刻。
当截断仍然失败时,停止增加权限
截断一个你能写的文件失败,并不能用目录权限解释,增加更多权限也无济于事。相反,要查看属性和限制:append-only 属性只允许以追加模式写入,只读挂载或服务沙箱可以拒绝 ACL 允许的操作。
检查单元的文件系统限制和该路径的挂载选项,然后复现失败的确切操作。放宽从未被查询过的权限,只会掩盖哪项限制才是真的。在一次性单元中每次只移除一项限制,并在做任何永久改动之前恢复它。