更换 VPN 证书颁发机构不是替换文件。新 CA 签下第一张证书的那一刻,每个仍然只信任旧 CA 的验证方都必须做出决定,而从未拿到新锚点的主机则继续拒绝合格的同伴。我见过它两个方向都出问题:网关学会了新 CA 而客户端没有,于是新连接全部失败;已经建立的隧道却继续存活,让这次中断看起来像路由问题。下面是会发生变化的地方,按故障出现的顺序排列。

一次更换,两处独立的改动

证书颁发机构是一个名字、一个公钥,以及用来签名的私钥。验证方从不抽象地信任“CA”。它们信任某个已经躺在信任库中的具体证书,并且只有当签名链能够抵达那份已保存的副本时才接受颁发者。因此更换 CA 会改变两件彼此独立失败的事情:接受哪些锚点,以及在这些锚点之下签发了哪些同伴身份。

大多数 VPN 部署在两个方向上都做验证。客户端验证网关的证书,网关验证客户端的证书。只刷新客户端包的更换会留下这样的服务器:它拒绝每一个仍然出示旧 CA 身份的客户端,反过来同样常见。先把执行点清点出来:客户端的信任库、网关的 ca 文件、它前面任何终止 TLS 的代理,以及从外部探测端点的探针。

验证方检查什么,按顺序

路径验证从同伴的证书一路向上,穿过任何中间证书,直到抵达一个受信任的锚点。每一步都验证签名、检查每张证书是否在有效期内,并确认基本约束与密钥用途允许该颁发者签名。直到这时,应用才去看叶子证书:主体备用名称必须覆盖被呼叫的名字,扩展密钥用途必须允许服务器或客户端认证。

若要在离线状态下重复这段走查,请把 -CAfile 指向你即将部署的那个包,而不是系统库。否则你测试的是主机,而不是这次改动:

openssl verify -CAfile new-ca.crt gateway.crt
openssl verify -CAfile new-ca.crt -untrusted intermediate.crt client.crt
openssl x509 -in gateway.crt -noout -subject -issuer -dates
openssl s_client -connect gw.example.net:443 -CAfile new-ca.crt

那条命令只对像网关 HTTPS 管理页面这样的简单 TLS 监听器有效;OpenVPN 的控制通道在 TLS 之前,因此 s_client 无法检查隧道的握手。

这些错误背后的路径构建规则写在 RFC 5280 第 6 节里。它们解释了那些否则显得任意的失败:一个没人记得的过期中间证书、一张密钥用途禁止签名的证书,或者一条把你在测试的主机排除在外的名称约束。

旧隧道为何还能继续工作

TLS 和 OpenVPN 都会在握手结束后让已建立的会话保持存活。没有任何东西会逐包地重新验证同伴证书,所以在改动之前连上的客户端会继续放行流量,直到它重新协商、重新连接或隧道崩溃。正是这个缺口让一次 CA 更换能在一小时内看起来正常,然后在最糟的时刻失败:笔记本从睡眠中醒来、NAT 映射过期、网关重启。

会话恢复把这个幻觉拉得更长。TLS 会话票据让客户端无需重新发送证书就能回来,而恢复会跳过路径验证,因为第一次握手已经做过了。因此诚实的测试是在新进程中做一次完整握手。请重启客户端,而不是重连隧道;在测试配置上考虑关闭票据,这样恢复路径就无法掩盖一条断掉的链。

把静态密钥层与这件事分开。tls-auth 给控制通道加上 HMAC,tls-crypt 对它加密并认证,两者都不携带证书材料,所以轮换其中任何一个都是带自己故障模式的独立改动。把两者混在一起的部署,最后会得到这样的客户端:静态密钥没问题、证书已过期,而错误信息一个都没提到。

复用主体名称的陷阱

为了下游什么都不用改而复用前任的可分辨名称,这很诱人。它让界面里的颁发者一行保持稳定,同时摧毁了你唯一需要的信号。两张主体相同、密钥不同的证书无法靠名字区分,所以一个同时带两个锚点的包会让验证方拿到两个候选,却没有选择规则。

于是路径构建可能选错,可能在正确的锚点就在眼前时报告“无法获取本地颁发者证书”,也可能在同一台主机上成功、在另一台主机上用同一个包失败,因为候选顺序不同。如果名字非复用不可,就在过渡期间把两个锚点放在不同文件里显式加载,并且等到每个同伴都出示新颁发者之后再合并。否则就给替代者一个独立的通用名——加个世代或日期后缀就够了——这样日志和错误文本才能说明失败的同伴是在哪个机构下签发的。

一次不会让网络瘫痪的轮换

对于计划内的更换,不要在单步内切换,而是在一个有限的时间窗内同时接受两个锚点。签发新 CA 及其中间证书,把新锚点放到每个验证方包里旧锚点旁边,然后才开始从它签发同伴证书。带旧身份的同伴有旧锚点来验证;新同伴走新锚点。

按这个顺序做,每一步都可回退。你要避免的故障是同时切换:错过了包更新的客户端遇上一个已经把旧锚点退役的网关,握手直接死掉,而没有任何日志说明是哪一侧过期了。

朝相反的方向退役。 重新签发每一个必须活下来的身份,确认每一个都出示新链,然后才从包里移除旧锚点。把旧 CA 的私钥撤出服务,因为一个还能签名的颁发者意味着轮换没有完成。保留证书和签发记录以备审计。丢失的颁发者可以恢复到同样的信任;一旦泄露就需要新身份,而且绝不回到旧锚点。

验证结果,而不是文件清单

信任文件待在预期目录里证明不了多少。应用可能读的是另一条路径、另一种格式,或者某个升级会替换掉的包内副本。要检查两端的行为,而不是文件是否存在。

确认用新身份的新连接能成功,然后确认由已退役 CA 签发的身份会被拒绝。第二个测试正是人们跳过的那个,也是唯一能说明旧机构不再放行的测试。在部署依赖 CRL 或 OCSP 的地方,检查新证书里的分发点能否解析,以及发问的同伴能否抵达响应方。

用一个从未连接过的客户端收尾:清掉它的配置状态,连接,然后读出协商出来的链。恢复缓存会话的同伴会兴高采烈地报告迁移成功,尽管它什么都没验证。

相关文章