tls-auth 失败通常被报告为握手问题,而它从来不是。HMAC 在任何 TLS 处理发生之前就在控制包上校验,因此密钥或方向不匹配会产生一个看起来像证书问题的失败,而它实际上在两层之前就已判定。把信封当作独立对象,诊断就会很短:读取字段,确认校验在早期触发,然后比较两端必须达成一致的三件事。

按信封的构造顺序读取它

控制包携带会话标识符、可选的 tls-auth HMAC、重放与确认信息,以及可靠的消息标识符,之后是消息类型负载。当消息是握手消息时,负载恰好是 TLS 密文。这就是全部关系:TLS 材料在一个信封已经保护的消息内部传输。参见 OpenVPN 网络协议参考,它按该顺序列出控制通道的字段。

可靠性层很容易被误认为密码学。确认、重传和消息标识符的存在,是为了让控制消息在不可靠传输上一次且按序到达。它们不是认证,正确的消息标识符也完全不能证明是谁发出了这个包。

把这种分组当作概念。它不是字节序示意图,也不足以计算标签,因为进入 HMAC 的字段取决于实现、版本和模式。

说明校验在握手之前触发

持有共享密钥的接收方可以校验 HMAC 并立即丢弃伪造包,无需分配握手状态,也无需进行公钥运算。防护同样覆盖最初的握手包,这正是错误密钥表现为连接失败、而不是迟到的认证错误的原因。

提高守护进程的详细级别,观察控制通道而不是 tun 设备:

openvpn --config /etc/openvpn/server.conf --verb 6

你要找的是在 TLS 告警之前、在包级别报告的控制通道 HMAC 校验失败。日志中出现 TLS 告警意味着信封已被接受、失败转入内层,那是另一种调查、另一种修复。

方向处理也在这里。当静态密钥文件与 tls-auth 一起使用时,两端取相反密钥方向,协议利用这种分离,使被捕获的包无法被反射回发送方。把方向弄反,密钥保持不变,但每个包仍然失败。一次只改一端,然后重启两个守护进程,让这对配置一起加载。

在两端对齐密钥、方向和 digest

三个值必须一致,而其中只有一个是文件。静态密钥内容在两端必须相同,方向必须相反,用于 HMAC 的 digest 必须一致。先检查配置再动密钥材料,因为一个反向的方向比损坏的文件能解释更多失败:

grep -E 'tls-auth|tls-crypt|key-direction|auth ' /etc/openvpn/server.conf

用工具而不是编辑器生成共享密钥,并保持权限严格:

openvpn --genkey secret /etc/openvpn/ta.key
chmod 600 /etc/openvpn/ta.key

较旧的版本接受略有不同的密钥生成选项顺序,为那些版本编写的配置文件仍在流传。两种情况生成的文件是同一类对象,所以要检查的是两端是否持有相同字节,而不是哪种写法生成了它。

不要把 tls-auth 与 tls-crypt 混淆。两者都保护控制通道,但 tls-crypt 还会加密它,并对观察者隐藏握手,并且用它自己的密钥处理取代了方向分离。使用其中之一的配置无法与为另一种配置的对端互通,再怎么调整 digest 也填不平这个差距。

只从真实格式复现标签

逐字节复现是与运行 VPN 不同的任务,它只在真实输入下成立:构建该包的实现与版本、所选的 digest、方向,以及真实字节(包括会话标识符和确认数组)。字段列表的文字说明不是那些字节。

标签长度由 digest 决定,所以固定长度的假设在有人改动它的那一刻就会失效。按实现的方式对包做哈希,而不是按博客文章里的表格所呈现的方式,并与同一对端取得的抓包比较,而不是与手工推导的预期比较。

把这项练习与生产配置分开。复现标签并不能告诉你某个部署是否应该开启防护。

在不移除防护的前提下回退不匹配

诱人的测试是删掉 tls-auth、看到连接成功,然后断定密钥错了。那个答案让你失去防护,也没有说明三个值里究竟是哪一个不一致。相反,应从服务器侧取得已批准的匹配配置并与之比较。

把变更准备好,使其可回退。保留先前的服务器配置和密钥文件,应用修正后的配对,验证客户端能完成握手,然后才删除备份。如果变更后客户端失败,恢复先前的文件,而不是在故障期间临时编出第三种变体。恢复应当是你开始之前就已经熟悉的一条命令。

最后,记住一次通过的校验意味着什么。它证明对端持有共享密钥,而不证明其证书或账户已被授权,并且 它对协商出的数据通道加密只字未提。授权与数据保护建立得更晚,所以干净的信封校验只是起点,不是判决。

相关文章