运行时间长只能证明启动时守护进程以当时持有的权限成功完成。真正重要的故障在后面:新客户端连入、吊销列表被替换、辅助脚本需要写文件。每一次都是无特权账户发起的新打开,有各自的权限问题和各自的错误信息。三个判断把能持续工作的配置与下一次事件就出错的配置区分开来。

把早期加载的内容与之后打开的内容分开

描述符是打开文件描述上的句柄,访问模式在该描述创建时就已固定。之后切换进程用户不会关闭句柄,所以以 root 读取的密钥文件在降权后仍能工作。模式也不会改变:为读而打开的句柄不会变成可写,可写的句柄也不会仅因账户失去对路径的权限而被收回。这两件事解释了这些报告中大部分出人意料的行为。

另一半是内存。守护进程可以解析证书、保留解析后的材料并关闭文件;继续被使用的是数据,而不是打开的文件。因此流量仍在继续,既不能证明描述符处于打开状态,也不能证明该路径名可达。它只证明当前会话所需的材料是更早取得的。

之后的打开是另一笔交易。它在当前凭据、当前安全上下文和当前挂载视图下运行,所以启动时可读的路径可能在下次连接时被拒绝。轮换还带来一个陷阱:在同一路径名上写入新文件会让已有的描述符仍绑定旧对象,于是重新加载看起来成功,而守护进程仍在用旧内容。在相信轮换生效之前,先比较进程持有的 inode 与磁盘上的 inode。

说出三处运行时读取以及各自的触发时机

有三个资源通常在初始化很久之后才被打开,而且失败的时刻各不相同。某个特定客户端的配置文件在该客户端连入时读取,因此它可以在其他人都正常时单独坏掉。吊销数据在对端认证时被查询,文件被替换时还会再查一次。临时目录只在脚本或插件要写东西时才被触碰。

这三者都不属于启动路径,所以健康数周的服务器也可能在下一次连接时失败。这个分类还点明了修复的归属:前两个是守护进程账户下对配置的读权限,第三个是一个用途很窄的可写位置。

Path reopened laterWhat triggers the openFirst sign when it fails
Client configuration fileA connection for that client identityOne client rejected while others connect
Revocation dataPeer authentication, including after replacementRevoked and valid peers both refused
Script or plugin temporary directoryA helper needs to create a fileThe helper exits with a path error

persist-key 回答的问题比多数人以为的更窄。它 在文档所述的 SIGUSR1 与 ping-restart 重置期间保持密钥文件打开,而对 CCD 文件、吊销数据、辅助可执行文件或完整进程重启没有任何作用。那些仍然走各自的打开路径,处于当时正在运行的任何账户之下。

确认账户以及它实际拥有的视图

不要信任 unit 文件,直接问正在运行的守护进程用的是哪个账户,再把答案与 unit 所设定的内容对照。不一致是第一件要解决的事,因为之后每一次权限检查都以进程真实身份运行。然后逐个组件地走过路径;穿越要在每一层评估,所以不可穿越目录里可读的文件依然读不到。最后的文件模式是最后一个问题,而不是第一个。

命名空间和服务沙箱增加了所有权与模式位无法描述的边界。你在 shell 里能看到的路径,在 chroot、挂载命名空间或隐藏家目录的 unit 里可能不存在,也可能只读。安全模块再加一层,所以看起来可读的文件在打开时仍可能被拒绝。

ps -o user:12,pid,args -C openvpn
systemctl show -p User -p Group -p ProtectHome -p RootDirectory [email protected]
sudo -u openvpn namei -l /etc/openvpn/ccd/alice

在管理员 shell 里读取成功并不检验上面那些条件的组合。以守护进程账户、在同样的命名空间和沙箱里运行检查,或者接受这个结果描述的是你的 shell 而不是服务器。第三条命令是这项测试的廉价版本:它打印每个组件的模式,并在守护进程账户无法穿越的第一个组件处失败。

给运行时写入一个单独的目录

读取只是清单的一半。辅助脚本可能需要创建状态文件,插件可能需要在配置旁边写状态,而这些写入并不会因为把配置树设为可写就被覆盖。给那个具体目录它功能所需的写权限,配上你能解释的所有者和模式。凡只是被读取的东西,都优先只读访问。

把私钥的权限与策略文件分开。重置后必须重读的密钥与每次连接都读取的客户端文件要求不同,而给两者同一个宽松模式会交出超过问题所需的访问。所有权很窄的专用守护进程账户比宽泛的组更容易审计。当密钥必须挺过重置时, persist-key 它取消重读,而不是放宽目录。

从你能复现的事件出发做判断

活着的隧道只证明代码里的一条路径。需要该文件的事件才是覆盖你所关心的那次打开的唯一测试,无论它是一次新的客户端连接、一次被替换的吊销文件、一次被允许的软重置,还是一次计划内的重启。选定事件,跑一遍,留住第一个错误而不是最后一个。

对软重置来说,一个信号和 unit 的日志就能看出密钥是否被重读。发出信号,然后读取 unit 写下的最后几行:

systemctl kill -s SIGUSR1 [email protected]
journalctl -u [email protected] -n 40 --no-pager

不要因为降权就断定被入侵的守护进程不能再改文件。它还能做什么,取决于剩余的凭据、它的能力、它能写入的目录,以及它仍持有的任何可写描述符。降低权限收窄了权力,而运行时打开的清单正说明收窄到哪一步。把这条边界写在配置旁边,下一个人就不必重新发现它。

相关文章