OpenVPN 中 RAND_bytes 失败:库错误或缺少设备节点
RAND_bytes 失败可能意味着熵源被拒绝,或设备节点被隔离环境隐藏。在创建设备之前,先读错误队列与进程视图。
RAND_bytes() failed 说的是一个本地原语,而不是账号、预共享密钥或远端端口。两种截然不同的故障会产生同一行输出,留下的证据也不同。一种是密码学后端拒绝输出字节;另一种是进程无法触及它当初为之编译的熵源。要区分两者,需要读一次错误队列,并看一眼被隔离进程自己的文件系统视图,而这项检查应排在 /dev 下创建任何东西之前。
返回值区分了什么
文档化的约定区分三种结果而非两种:成功返回 1,加载的 RAND 方法不支持请求时返回 -1,其他失败返回 0,解释留给错误队列。RAND_bytes 参考文档补充了一个容易被略过的点:在受支持的平台上,默认生成器从操作系统取得种子;当取种失败时,它停留在错误状态,而不是交出可预测的字节。
这些数字里只有一个描述环境。返回 -1 属于构建、提供方或功能问题,因为代码请求了当前实现并不提供的东西。带着取种抱怨返回 0 属于环境问题,因为生成器根本没能取得熵。单看数字永远说不清是哪一种,函数名同样说不清。
隐藏的设备节点何时才是真正原因
受限服务看到的目录树与你敲命令的 shell 不同。chroot 监狱、容器镜像和加固后的 unit 文件经常呈现出一个节点更少的 /dev。如果失败实现按路径名打开 /dev/urandom,这点差异就足以让启动失败,而宿主机本身看起来完全健康。监狱之外存在的设备,不能证明被隔离的进程有权打开什么。
不过并非每种随机接口都需要路径。Linux 的 getrandom 系统调用既不接受文件名,也不接受文件描述符,其手册页指出这一特性在 chroot 隐藏设备路径的地方恰好有用。
所以“缺少节点”只是关于代码某一条分支的假设,而不是关于随机数的通则。证据指名路径时它成立,证据指名别的东西时它就只是猜测。
先读错误队列,再看进程视图
最快弄清问题的方式,是先读库已经记录了什么,再直接查看监狱内部的视图。先从 服务实际加载的 OpenSSL 构建 开始,因为提供方栈在主版本与发行版之间并不相同。然后再问 systemd 哪个进程在运行、对它施加了哪些限制。只有这两件事都摆到桌面上,列出该进程能看到的节点才有意义。
openssl version -a
systemctl show openvpn-server@lab -p MainPID -p PrivateDevices -p DevicePolicy
pid=$(systemctl show -p MainPID --value openvpn-server@lab)
sudo ls -l /proc/"$pid"/root/dev/urandom /proc/"$pid"/root/dev/random
把这些读取动作施加在同一个进程上,而不是碰巧开着的 shell 上。把宿主机的 /dev 与一个不相干进程的视图作比较,正是错误依赖被修两遍的原因。
直到能指名它,才去添加依赖
当证据确实指出了一个路径,修复也很小:让那一个路径在监狱内可用,其余一律不动。对单个节点做 bind 挂载通常就够;在不方便挂载的环境里,用 mknod 配合该节点的标准 major、minor 号能得到同样的结果。放开沙箱、为所有字符设备授权或关闭 PrivateDevices,是用长期的安全回退换一次定点修复。
熵守护进程不能替代那项修复。像 haveged 这样的软件包改变的是内核池的供给方式,它们无法造出一个被隔离进程有权打开的路径。装上一个也许能让健康曲线好看些,而 open 调用依旧失败。把两个问题分开,否则下一次故障又会被归到错误的层。
分别验证生成器与隧道
重启是唯一会复用同一份配置、同一个 unit 文件、同一套隔离的测试,正是它们造成了故障。执行重启,然后读日志里最新的条目,而不是早先一次启动的样本。
sudo systemctl restart openvpn-server@lab
sudo journalctl -u openvpn-server@lab --since -5min --no-pager | grep -iE 'rand|entropy|Initialization Sequence'
在这份输出里找两件彼此独立的事:随机源相关消息消失,以及隧道到达惯常的启动里程碑。重新可用的生成器只消除一个本地故障,对服务器身份、凭据或路由什么也没说。用此前失败过的同一份客户端配置去测,结果才可比,而不只是更好。