降权之后:哪些 OpenVPN 文件仍在工作?
降权前打开的描述符继续可用,之后重新打开的路径则不行。判断你的配置需要重新打开三个运行时文件中的哪一个。
运行时间长只能证明启动时守护进程以当时持有的权限成功完成。真正重要的故障在后面:新客户端连入、吊销列表被替换、辅助脚本需要写文件。每一次都是无特权账户发起的新打开,有各自的权限问题和各自的错误信息。三个判断把能持续工作的配置与下一次事件就出错的配置区分开来。
把早期加载的内容与之后打开的内容分开
描述符是打开文件描述上的句柄,访问模式在该描述创建时就已固定。之后切换进程用户不会关闭句柄,所以以 root 读取的密钥文件在降权后仍能工作。模式也不会改变:为读而打开的句柄不会变成可写,可写的句柄也不会仅因账户失去对路径的权限而被收回。这两件事解释了这些报告中大部分出人意料的行为。
另一半是内存。守护进程可以解析证书、保留解析后的材料并关闭文件;继续被使用的是数据,而不是打开的文件。因此流量仍在继续,既不能证明描述符处于打开状态,也不能证明该路径名可达。它只证明当前会话所需的材料是更早取得的。
之后的打开是另一笔交易。它在当前凭据、当前安全上下文和当前挂载视图下运行,所以启动时可读的路径可能在下次连接时被拒绝。轮换还带来一个陷阱:在同一路径名上写入新文件会让已有的描述符仍绑定旧对象,于是重新加载看起来成功,而守护进程仍在用旧内容。在相信轮换生效之前,先比较进程持有的 inode 与磁盘上的 inode。
说出三处运行时读取以及各自的触发时机
有三个资源通常在初始化很久之后才被打开,而且失败的时刻各不相同。某个特定客户端的配置文件在该客户端连入时读取,因此它可以在其他人都正常时单独坏掉。吊销数据在对端认证时被查询,文件被替换时还会再查一次。临时目录只在脚本或插件要写东西时才被触碰。
这三者都不属于启动路径,所以健康数周的服务器也可能在下一次连接时失败。这个分类还点明了修复的归属:前两个是守护进程账户下对配置的读权限,第三个是一个用途很窄的可写位置。
| Path reopened later | What triggers the open | First sign when it fails |
|---|---|---|
| Client configuration file | A connection for that client identity | One client rejected while others connect |
| Revocation data | Peer authentication, including after replacement | Revoked and valid peers both refused |
| Script or plugin temporary directory | A helper needs to create a file | The 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不要因为降权就断定被入侵的守护进程不能再改文件。它还能做什么,取决于剩余的凭据、它的能力、它能写入的目录,以及它仍持有的任何可写描述符。降低权限收窄了权力,而运行时打开的清单正说明收窄到哪一步。把这条边界写在配置旁边,下一个人就不必重新发现它。