服务启动了,读取配置时没有任何抱怨,随后却倒在一行看似无关的日志上:它打不开 ca.crt。证书本身没有变化,unit 文件里的配置路径是绝对路径。这一组合就是线索。你传入的路径只定位了一个文件,而该文件内部的名字却在完全不同的位置被解析,所以一次看起来完全合格的启动,仍可能从没人打算使用的目录里读取密钥。

这种失败的原因不多,值得按顺序区分:进程实际拥有的基准目录、服务的启动上下文,以及路径正确之后文件周围的权限或沙箱。每一种都会留下不同的痕迹——在 unit 文件里、在进程里、在文件系统上。

配置路径不是基准目录

设想一个在 /tmp 中启动、并被交给 /etc/vpn/prod/tenant-a.ovpn 的进程。在那个配置里,ca ca.crt 这一行写的是相对路径。定位配置的那个参数并不会改写这个名字,于是进程去找 /tmp/ca.crt,并报告文件缺失。

令人困惑的是报错的顺序。OpenVPN 先解析配置,一边读一边打开被引用的文件,所以配置里的拼写错误会在任何密钥被触碰之前就报出来,而缺失的证书要等解析走到那一步才出现。因此配置能够加载,只能证明语法没问题,并不能证明它点名的文件都已被找到。

手册区分了什么:--config 与 --cd

OpenVPN 2.6 手册描述了两个不同的选项。一个加载配置文件。另一个在读取配置、密钥、脚本及其他文件之前切换进程目录,并且要求给出不引用当前目录的绝对路径。

openvpn --cd /etc/vpn/prod --config tenant-a.ovpn

这个调用只是一种形式,并不是叫你去启动别人的配置。它带来的是确定性:链条中每个相对名字的基准都写在命令里,而不是从启动进程的某个 shell 或管理器那里继承。另一种做法同样有效,对服务而言往往更好:不要改动工作目录,把配置中引用的每个文件都写成绝对路径。

无论选哪种,都要审计整条链条,而不只是第一个失败。配置可以引入另一个配置文件,被引入文件里的相对名字是按同一个进程目录解析的,而不是按引入它的那个文件所在目录解析。日志路径、认证文件和辅助脚本都遵循同一条规则。

服务从别处启动

在配置目录里开一个交互式 shell,对打包好的 unit 毫无证明力。管理器会设定自己的工作目录,包装脚本还可能在二进制运行前再改一次。真正失败的那个 unit,才是唯一值得推敲的启动方式。

systemctl show -p WorkingDirectory -p ExecStart --value openvpn@tenant-a
ls -l /proc/$(pgrep -n openvpn)/cwd

第二条命令显示的是活进程实际持有的目录;当存在包装脚本时,它比 unit 文件更可信。如果两者不一致,答案就是包装脚本,unit 只是误导。

查找之后:权限、chroot 与沙箱

基准目录正确,仍可能报出同一条消息,因为解析路径和访问文件是两个步骤。进程需要对每个父目录的执行权限、对文件的读权限;而一个遮蔽了部分文件系统的服务 unit,会直接改变进程能看到什么。

三种常见情形会给出完全相同的错误。文件只对 root 可读,而服务在打开它之前就放弃了特权。unit 设置了保护性的文件系统视图,其中不包含配置目录。守护进程带 chroot 运行,于是主机上存在的路径在那个视图里并不存在。

用文件所有者和进程身份去诊断,而不是放宽权限。为了让这条错误闭嘴而把私钥设成所有人可读,是把响亮的失败换成安静的失败,而密钥恰恰是目录里你最不愿暴露的部分。

用你自己的启动来验证修复

从一个与配置无关的目录重启真正的 unit,观察同一条启动日志。然后确认进程在运行期间持有的是预期目录,并且它加载的证书正是你所期望的那一个。

systemctl restart openvpn@tenant-a
systemctl status openvpn@tenant-a --no-pager
journalctl -u openvpn@tenant-a -n 20 --no-pager

路径加载成功之后,认证和路由仍是各自会失败的独立阶段;连接能起来却不通流量,是另一项排查。这里的修复只关乎文件来自哪个目录,而且它应该体现在调用里,而不是留在上次手工执行者的记忆里。

如果配置由配置管理工具下发,就把基准目录或绝对文件名也写在那里。只在单台主机上手工修正,正是这个问题在下次重建后会回来的版本。

相关文章