让 VPN 根 CA 保持离线,同时照常签发证书
将根密钥留在网络之外,用中间 CA 签发叶子证书,按时发布 CRL,并在第一个客户端连接前测试整条链。
你的 VPN 在凌晨 3 点仍在应答连接,而为其证书签名的机器却关着躺在抽屉里。验证服务器证书的客户端从不联系你的 CA,因此 CA 不必对任何连接可达。真正需要规划的是:某天必须签发、续期或吊销证书,而根密钥始终锁在保险柜里。
一次连接实际验证什么
OpenVPN 客户端连接时,会沿着签名链从服务器叶子证书一路向上,直到它已经信任的 CA 证书,检查有效期,并在配置文件要求时查阅吊销列表。它从不查找你的根 CA。服务器对客户端证书做同样的镜像操作。连接发生时,在场的只有叶子、中间证书和你此前分发的信任包。
真正失败的是三种情况之一:签发时签名密钥不可用、吊销列表过期,或者客户端的信任包比服务器呈现的链更旧。第一种是流程问题,后两种是分发问题。它们都不要求根密钥在线;要求用私有证书做隧道认证的托管 VPN 服务只是把签名权限交给了提供商。Azure 点到站点则相反:你上传根证书,其下签发的证书即被信任,所以要问清谁签名、谁验证,以及证书如何提前撤回。
在永远得不到地址的主机上构建根
离线根很少使用:只在签发中间 CA 证书以及日后续期或吊销它时用。其余一切都在下一层完成。在一台不接入网络的机器上完成根的工作,尽可能使用 live 系统,并记录谁在场。命令假定 OpenSSL 1.1.1 或更新版本,因为旧版本没有 -addext。
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out root.key
openssl req -x509 -new -key root.key -sha256 -days 7300 \
-subj "/CN=Example VPN Root CA" -out root.crt \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
在签名主机上创建中间证书,只把请求带到离线机器。根以路径长度为零签发该请求,因此中间 CA 可以签发终端证书,但不能在其下再建一个 CA。正是这一个扩展,阻止被攻破的签名主机制造与根同级的权威。仅当所有客户端都运行 OpenVPN 2.4 或更新版本时才使用 ECDSA;否则生成 RSA 密钥并保持流程不变。
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out intermediate.key
openssl req -new -key intermediate.key -subj "/CN=Example VPN Intermediate CA" -out intermediate.csr
openssl x509 -req -in intermediate.csr -CA root.crt -CAkey root.key \
-CAcreateserial -days 3650 -sha256 -extfile intermediate.ext -out intermediate.crt
intermediate.ext 包含 basicConstraints=critical,CA:TRUE,pathlen:0、keyUsage=critical,keyCertSign,cRLSign 和 authorityKeyIdentifier=keyid:always。在重新连接签名主机之前,把根密钥放回保险柜。如果根必须能吊销中间证书,就用 openssl ca -config root.cnf -extensions intermediate_ca 对根级 index.txt 签名,而不是用 openssl x509 -req,以便记录序列号;然后把生成的根列表放进客户端配置里,由基于文件的 crl-verify 在启动时读取。发给客户端的包要包含两张 CA 证书;只有叶子证书无法对根完成验证。
从中间 CA 签发叶子证书,并配合数据库
用 openssl x509 -req 签名在需要吊销之前一直可用,因为没有任何东西记录序列号。从第一张证书起就使用 openssl ca,让序列号、到期时间和吊销状态都保存在同一个数据库中。数据库文件只需创建一次,然后把配置指向它们:
mkdir -p /etc/vpn-ca/intermediate/newcerts
touch /etc/vpn-ca/intermediate/index.txt
echo 1000 > /etc/vpn-ca/intermediate/serial
echo 1000 > /etc/vpn-ca/intermediate/crlnumber
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /etc/vpn-ca/intermediate
database = $dir/index.txt
new_certs_dir = $dir/newcerts
serial = $dir/serial
crlnumber = $dir/crlnumber
certificate = $dir/intermediate.crt
private_key = $dir/intermediate.key
default_md = sha256
default_days = 365
default_crl_days = 30
unique_subject = no
policy = policy_any
[ policy_any ]
commonName = supplied
[ server_cert ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = @server_names
[ client_cert ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
[ server_names ]
DNS.1 = vpn.example.com
密钥用途那行假定使用 ECDSA 密钥;RSA 需要加上 keyEncipherment。给每个客户端单独的公钥名,因为 OpenVPN 用该字段识别连接,两台共用 CN=vpn-client 的笔记本在日志、client-config-dir 规则或任何按用户推送中都区分不开,你也就失去了让吊销决定站得住脚的归属信息。像这样签发并验证叶子证书:
openssl req -new -key alice.key -subj "/CN=alice-laptop" -out alice.csr
openssl ca -config intermediate.cnf -extensions client_cert -in alice.csr -out alice.crt -batch
openssl verify -CAfile ca-bundle.crt -untrusted intermediate.crt alice.crt
把签名密钥放在策略边界之后,而不是复制到 VPN 主机:由一个小服务验证请求、按允许列表核对公钥名,只返回证书。记录每个序列号;日志是证书与申请人之间唯一的联系。
在不唤醒根的情况下发布吊销
客户端证书由中间 CA 吊销,绝不由根吊销。根列表只有当中级 CA 被烧毁时才重要。用 crl-verify 让服务器指向中间列表。基于文件的 CRL 在服务启动时加载,所以每次重新生成后都要重新加载;目录形式在连接时读取状态,每个序列号占用一个文件。
从同一个数据库用两条命令吊销并重新生成:
openssl ca -config intermediate.cnf -revoke /etc/vpn-ca/intermediate/newcerts/1001.pem
openssl ca -config intermediate.cnf -gencrl -out /etc/openvpn/crl.pem
然后盯住时钟。CRL 带有下次更新字段,一旦该日期过去,验证方会拒绝该列表而不是忽略它。用 default_crl_days = 30 按月重新生成留有余量,按年则没有。叶子证书寿命更短是更可靠的控件,因为吊销只在查阅列表的地方才起作用。
测试整条链,然后把仪式写下来
在信任设计之前先复现四种失败。用 openssl verify -CAfile ca-bundle.crt -untrusted intermediate.crt server.crt 验证链。用过期客户端证书连接,确认服务器拒绝。吊销一个测试客户端,重新加载,确认列表生效。把某台主机的时钟拨快一天,因为错误的时钟可能签发或拒绝在别处看起来有效的证书。
离线根只有在文书存在时才是控制措施:仪式需要两名具名人员、每次签发的序列号都有记录、加密备份与机器分开存放,并在隔离主机上做恢复测试。在同一次仪式里再签一张备用中间证书,这样签名主机坏掉时你不必半夜去开根。
在宣布完成之前,确认五件事:根密钥不在联网主机上、中间密钥只存在于签名主机、客户端包包含两张 CA 证书、CRL 有未来的下次更新时间,并且链上每个到期日都有人负责日历条目。缺一项,看似离线的部署就只是没有文档而已。