日常运维中,仅仅依靠用户名密码来保护Nginx背后的管理后台或内部API,安全性往往不够。一旦密码泄露,服务就完全暴露。双向TLS认证的思路是:服务端除了出示自己的证书外,还要求客户端出示一张由可信CA签发的证书,没有证书或证书不合法的连接直接被拒绝。easy-rsa是OpenVPN社区提供的证书管理脚本,把复杂的openssl命令封装成了简单的脚本调用,用它来搭建私有CA并签发证书非常方便。本文完整演示从CA搭建到Nginx双向认证配置的全过程。

用easy-rsa搭建私有CA并签发证书
easy-rsa的安装很简单,可以直接从GitHub下载发布包,也可以通过系统包管理器安装。以Linux环境为例,解压后进入目录,初始化PKI环境并构建CA:
# 下载并解压 easy-rsa wget https://github.com/OpenVPN/easy-rsa/releases/download/v3.1.7/EasyRSA-3.1.7.tgz tar xzf EasyRSA-3.1.7.tgz cd EasyRSA-3.1.7 # 初始化PKI目录结构 ./easyrsa init-pki # 构建CA,nopass表示CA私钥不加密,生产环境建议去掉nopass ./easyrsa build-ca nopass
执行build-ca时会提示输入Common Name,这是CA的名字,可以随意填写比如MyCorp CA。完成后在pki目录下会生成ca.crt和private目录下的ca.key,这两份文件就是整个信任体系的根,必须妥善保管,尤其是ca.key,一旦泄露任何人都能伪造证书。
接下来签发服务端证书和客户端证书。服务端证书的Common Name填域名,客户端证书的Common Name填用户标识或设备标识,便于后续识别与吊销:
# 签发服务端证书,nginx-server是自定义名称 ./easyrsa gen-req nginx-server nopass ./easyrsa sign-req server nginx-server # 签发客户端证书,user1是客户端标识 ./easyrsa gen-req user1 nopass ./easyrsa sign-req client user1 # 生成 Diffie-Hellman 参数(TLS 1.2及以下需要) ./easyrsa gen-dh
签发完成后,关键文件位置如下:服务端证书在pki/issued/nginx-server.crt,服务端私钥在pki/private/nginx-server.key,客户端证书在pki/issued/user1.crt,客户端私钥在pki/private/user1.key,CA证书在pki/ca.crt。需要把这些文件分发到对应的服务器和客户端机器上。
Nginx双向认证的完整配置
Nginx中控制客户端证书验证的核心指令是ssl_verify_client。设置为on表示强制要求客户端证书,验证失败直接握手失败;设置为optional则不强制,可以通过变量判断证书状态后在应用层做访问控制,适合既要开放公开页面又要保护部分路径的场景。
server {
listen 443 ssl;
server_name admin.ippipp.com;
# 服务端证书
ssl_certificate /etc/nginx/ssl/nginx-server.crt;
ssl_certificate_key /etc/nginx/ssl/nginx-server.key;
# 客户端证书验证
ssl_client_certificate /etc/nginx/ssl/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
# TLS协议与加密套件
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:8080;
# 把客户端证书信息传递给后端
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Serial $ssl_client_serial;
}
}
这里注意几个细节。ssl_client_certificate指向的是CA证书而不是客户端证书,Nginx用它来验证客户端证书的签名链。ssl_verify_depth限制证书链的深度,如果是根CA直接签发客户端证书,设为1或2即可。配置完成后先用nginx -t检查语法再reload:
nginx -t nginx -s reload
验证效果时,如果没有客户端证书直接用浏览器或curl访问,会收到TLS握手失败的提示。携带客户端证书访问则能正常返回内容:
# 无证书访问,会被拒绝
curl https://admin.ippipp.com/
# 携带客户端证书访问,成功
curl --cert user1.crt --key user1.key https://admin.ippipp.com/
# 也可以把证书和私钥合并成pfx格式导入浏览器
openssl pkcs12 -export -out user1.pfx \
-inkey pki/private/user1.key \
-in pki/issued/user1.crt \
-certfile pki/ca.crt
可选模式与证书吊销的进阶用法
强制模式下,没有证书的请求连HTTP层都进不来,如果希望同一个server块既服务公开页面又保护敏感路径,可以把ssl_verify_client改为optional,然后在location中根据$ssl_client_verify变量判断:
server {
listen 443 ssl;
server_name mixed.ippipp.com;
ssl_certificate /etc/nginx/ssl/nginx-server.crt;
ssl_certificate_key /etc/nginx/ssl/nginx-server.key;
ssl_client_certificate /etc/nginx/ssl/ca.crt;
ssl_verify_client optional;
location /admin/ {
# 只有证书验证通过才放行
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_pass http://127.0.0.1:8080;
}
location / {
proxy_pass http://127.0.0.1:8080;
}
}
$ssl_client_verify在验证通过时值为SUCCESS,失败时为FAILED或NONE,除了它还有$ssl_client_s_dn可以拿到客户端证书的主体名称,$ssl_client_fingerprint可以拿到证书指纹,利用这些变量甚至能实现基于特定用户证书的白名单控制。
证书吊销是双向认证运维中不可忽视的一环。员工离职或设备丢失时,必须让对应的客户端证书立即失效。easy-rsa通过吊销列表CRL来管理,Nginx加载CRL文件后会在握手时检查证书状态:
# 吊销 user1 的证书 ./easyrsa revoke user1 # 生成或更新CRL文件 ./easyrsa gen-crl
生成的CRL文件位于pki/crl.pem,把它复制到服务器上并在Nginx中启用:
ssl_crl /etc/nginx/ssl/crl.pem;
reload之后被吊销的证书就无法再通过验证了。需要注意Nginx加载CRL后,所有客户端证书都会被检查吊销状态,所以每次吊销新证书都要同步更新服务器上的crl.pem。
常见问题与运维建议
实际部署中最常遇到的报错是400 No required SSL certificate was sent,说明客户端没有携带证书,或者携带的证书链不完整。特别是浏览器场景,导入pfx时一定要包含CA证书,否则浏览器只会发送终端证书而中间CA缺失,服务端验证链会失败。curl测试时如果证书是中间CA签发的,需要用--cacert参数指定完整信任链。
另一个常见问题是证书过期。easy-rsa默认签发的证书有效期为825天,到期后客户端会突然无法访问。建议建立证书台账记录每张证书的到期时间,续期流程和签发一样,重新gen-req加sign-req即可,客户端换装新证书。也可以在签发时通过easyrsa的EASYRSA_CERT_EXPIRE变量自定义有效期。
安全方面再强调几点。CA私钥务必离线保管,签发证书的机器和运行Nginx的机器尽量分离;客户端私钥建议生成时设置密码保护,虽然使用时多一步输入,但能防止证书文件被拷贝后直接滥用;对安全要求极高的场景,还可以结合Nginx的error_page机制,把验证失败的请求引导到提示页面而不是直接断开。把这套体系搭建好之后,Nginx后面的服务就多了一道比密码强得多的准入门槛,配合防火墙规则,基本可以做到只有特定的人和设备才能访问内部服务。