教程里看起来相似的两个目录,承担完全不同的职责。一个保存证书颁发机构:它的私钥、序列号计数器,以及它签发过的一切的记录。另一个保存守护进程启动并向客户端提供服务所需的内容。把两者放在同一台主机、同一个账户下,一个下午很方便,却会在部署余下的整个生命周期里代价高昂,因为守护进程此后就与那把能签发任何身份的密钥比邻而运行。

CA 工作区里有什么

PKI 目录是签发者的状态,而不是配置。它包含 CA 的私钥和证书、序列号文件、已签发证书的索引,以及由它们生成的每个身份的材料。

签署请求会改变这一状态,因此工作区应属于不使用 VPN 服务的主机上、由管理员控制的账户。凡能读取 CA 私钥的人都能签发服务器会信任的证书,所以对这把密钥的保护就是整个信任模型,且只落在一个文件上。为省一步而把它复制到第二个位置,等于多出一个要保护的东西和一个会被遗忘的东西。每次签名还会递增序列号并向索引追加一条记录,所以工作区是一份状态副本,而不是一堆文件。

公开的 CA 证书不同。它是用来分发的,客户端和服务器正是用它互相验证。

守护进程启动时读什么

运行中的服务器只消费一小部分内容:它自己的证书、它自己的私钥、用于验证对端的 CA 证书,以及所选模式需要的密钥材料。它在启动时读取这些路径,并在重新加载时再读一次。

归属应当以真正启动的那个单元为基准来评估,而不是以交互式 root shell 为基准。声明的沙箱化会使文件系统的某一部分即使以 root 手工测试成功,也变成只读或不可见。

systemctl show -p User -p Group -p ReadOnlyPaths [email protected]
stat -c '%a %U:%G' /etc/openvpn/server/server.key
runuser -u openvpn -- test -r /etc/openvpn/server/server.key; echo $?

第三行正是人们会跳过的那一行。它问的是守护进程自己的账户能不能读取该密钥,而这才是权限被降下之后唯一有意义的问题形式。以 root 身份看到退出码为零,并不能说明降权之后会发生什么。

改任何权限之前,值得把两条故障路径分开。签名失败指向 PKI 目录、其父目录的搜索权限,以及运行签发者的账户。启动失败指向单元、它的工作目录,以及服务器配置中写明的路径。把整棵树都设为所有人可读,既治不了其中任何一条,又抹掉了两者共同依赖的边界。

为什么把 CA 密钥复制到服务器之后会出问题

副本通常始于一次调试:有人在 VPN 主机上要续期客户端,于是 CA 密钥落进 /etc/openvpn,续期生效了。眼前的任务完成了,边界也没有了。

随后服务器被备份,签发机构随之进入备份。一份快照、一个支持包,或者配置目录的一份拷贝,如今都带着在 CA 有效期内签发可信身份的能力。持有那份拷贝的人可以为服务器接受的任何名字签发证书,包括从未签发过的名字。而这次签发的唯一记录,就是他们选择保留的那一份。

从那种状态恢复,并不是删掉一个文件的事。你得判断密钥是否外泄,外泄就轮换 CA,并把信任重新分发到每一个对端。独立的工作区可以省掉这整套讨论。

账户、目录、备份

给签发者独立的账户和家目录,条件允许就放在 VPN 主机之外。给守护进程另一个账户,只对已部署材料有读取权限,对任何签发证书的东西都没有写入权限。部署时把公开证书往外复制,绝不要把私钥状态往里拉。两个角色分别备份,因为它们的敏感程度和恢复故事不同。

吊销之后,更新服务器配置所指的 CRL 文件,然后重新加载服务,让新连接读到它。Easy-RSA 文档介绍了工作区的布局和签名流程;围绕它的归属规则仍然是对你这台主机的决定。用 openssl crl -in /etc/openvpn/crl.pem -noout -lastupdate -nextupdate 去确认服务究竟加载了什么,而不是假定重新加载已经把它取了进来。

证明分离的测试

要检查反面情形,而不只是能跑通的情形。按设计从签发者账户签发一张证书,然后确认同一个账户无法启动服务,也无法读取服务器的私钥。接着确认守护进程的账户无法读取 CA 私钥。

这三项检查花几分钟,而且能挺过人员更替,因为它们陈述的是权限,而不是习惯。把结果连同覆盖到的路径、账户,以及执行的日期一起写下来。否则接手这套部署的下一个人,又会从一份从来没有边界可言的教程里把边界重新推导一遍。带日期的记录也是让下一次审查只需几分钟而不是一下午的原因。

相关文章