Nginx作为最流行的反向代理服务器之一,其SSL/TLS模块不仅支持服务端证书,还支持对客户端进行证书验证,这就是所谓的双向认证(mTLS)。相比单向认证,mTLS能让服务端确认客户端身份,广泛应用于企业内部微服务调用、API开放平台、物联网设备接入等场景。本文将围绕ssl_verify_client指令展开,从配置深度、验证原理到常见错误排查,帮你彻底掌握这套机制。

一、理解mTLS与ssl_verify_client的四种模式
单向HTTPS认证中,只有客户端验证服务端证书;而双向认证要求客户端也出示证书,由服务端验证其合法性。在Nginx中,这一切由ssl_verify_client指令控制。它只能出现在http和server块中,有四个可选值,理解它们的差异是正确配置的前提。
设置为on时,Nginx强制要求每个连接必须提供客户端证书且证书必须通过验证,否则握手直接失败。这是最严格的模式,适合全站强认证场景。设置为off则完全不验证客户端证书。
真正体现深度配置的是两个可选模式:optional要求客户端必须响应服务端的证书请求(certificate request),但即使证书验证失败也允许连接继续,验证结果会写入$ssl_client_verify变量,业务层可以据此做细粒度判断;optional_no_ca则更宽松,客户端甚至可以不发送证书,Nginx不会因为缺少证书而拒绝连接。
server {
listen 443 ssl;
server_name api.ipipp.com;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_client_certificate /etc/nginx/ssl/ca.crt;
# 强制验证:无证书或验证失败直接拒绝
ssl_verify_client on;
# 验证深度:允许客户端证书链最多3层中间CA
ssl_verify_depth 3;
# 校验证书用途,确保证书用于客户端认证
# ssl_verify_client on 时不检查 extendedKeyUsage,需结合业务自行校验
}注意ssl_verify_depth的默认值为1,表示只验证客户端证书的直接签发者。如果客户端证书由二级甚至三级CA签发,而验证深度不够,握手会失败,这是生产环境最常见的坑之一。
二、CA证书配置与证书链校验细节
ssl_client_certificate指定的文件中包含的CA证书列表,既是验证客户端证书的信任锚,也会通过TLS握手下发给客户端,供客户端挑选合适的证书。而ssl_trusted_certificate则只用于验证证书链和OCSP stapling,不会下发给客户端。当你的信任CA较多但不想全部暴露给客户端时,可以用前者放根CA、后者补充中间CA。
证书链不完整是验证失败的高频原因。假设你的CA结构是根CA签发中间CA,中间CA签发客户端证书,那么ssl_client_certificate文件中应包含根CA和中间CA。如果只放了根CA,客户端又只发送了叶子证书而没有附带中间证书,Nginx将无法构建完整链路,握手会以失败告终。解决办法有两个:要么在信任文件中补齐中间CA,要么在生成客户端证书时把中间CA打包进p12文件。
# 合并根CA与中间CA到信任文件
cat root-ca.crt intermediate-ca.crt > /etc/nginx/ssl/ca-chain.crt
# 生成包含完整链路的客户端p12(客户端会随证书发送中间CA)
openssl pkcs12 -export -in client.crt \
-inkey client.key \
-certfile intermediate-ca.crt \
-out client-full.p12
# 用curl自测,指定客户端证书与私钥
curl -v --cert client.crt --key client.key \
--cacert root-ca.crt https://api.ipipp.com/user/info验证证书是否由特定CA签发时,可以先用openssl检查链路:执行openssl verify -CAfile ca-chain.crt client.crt,如果本地验证通过而Nginx仍报错,问题多半出在ssl_verify_depth或证书有效期、密钥用途上。另外要留意证书的extendedKeyUsage扩展,某些工具签发的证书只标记了serverAuth,部分严格客户端会拒绝将其用于客户端认证。
三、内嵌变量与自定义错误处理
当使用optional模式时,Nginx会暴露一批与客户端证书相关的内嵌变量,业务层可以据此实现身份识别和权限控制。$ssl_client_verify的取值有三种:NONE表示客户端未提供证书,SUCCESS表示验证通过,FAILED后面会跟失败原因。常见的判断写法如下:
server {
listen 443 ssl;
ssl_verify_client optional;
# ...证书配置省略...
location /api/ {
# 未通过验证的请求统一返回401
if ($ssl_client_verify != SUCCESS) {
return 401;
}
# 从证书主题中提取CN作为用户标识传递给后端
proxy_set_header X-Client-Id $ssl_client_s_dn_cn;
proxy_set_header X-Client-Fingerprint $ssl_client_fingerprint;
proxy_pass http://backend;
}
location /public/ {
# 公开接口跳过验证
proxy_pass http://backend;
}
}除主题DN外,$ssl_client_i_dn提供签发者信息,$ssl_client_serial是证书序列号,$ssl_client_fingerprint是证书SHA1指纹,$ssl_client_v_start、$ssl_client_v_end、$ssl_client_v_rem则分别表示证书起止时间和剩余天数,可用于提醒证书即将过期。如果需要传递完整证书给后端,$ssl_client_escaped_cert是URL编码后的完整PEM,比已废弃的$ssl_client_cert更适合放在HTTP头中。
当ssl_verify_client on且验证失败时,Nginx默认返回无正文的400错误,排查体验很差。可以配合error_page和495、496内部状态码返回更友好的信息:495对应证书校验错误,496对应未提供证书。示例如下:
error_page 495 =495 /cert_error;
error_page 496 =496 /no_cert;
location = /cert_error {
default_type application/json;
return 495 '{"code":495,"msg":"client certificate verify failed"}';
}
location = /no_cert {
default_type application/json;
return 496 '{"code":496,"msg":"client certificate required"}';
}四、常见错误排查思路总结
排查mTLS问题时建议按固定顺序定位。第一步确认端口握手层:用openssl s_client -connect api.ipipp.com:443 -cert client.crt -key client.key -CAfile root-ca.crt连接,观察输出中的Verify return code,这是最接近真相的调试方式。第二步检查Nginx错误日志,握手失败一般会记录SSL_do_handshake failed并附带错误码,例如unable to get local issuer certificate提示链路不完整,certificate has expired提示证书过期。
第二步之后的常见错误码要熟记:400 Bad Request accompanied by The SSL certificate error对应495,即证书提供了但验证失败,重点检查CA文件、证书链和验证深度;400 accompanied by No required SSL certificate was sent对应496,即客户端没有发送证书,检查客户端是否正确加载了证书以及TLS版本是否支持;如果返回200但$ssl_client_verify为NONE,说明配置的是optional_no_ca或者证书请求未被客户端响应,此时业务层的校验就成了唯一防线,千万不要遗漏。
最后提醒两个容易忽视的点:一是修改证书相关配置后必须nginx -s reload才会生效,且OpenSSL版本对TLS1.3下客户端证书请求的处理有差异,老版本Nginx搭配TLS1.3可能出现optional模式下收不到证书的怪异现象,升级通常可以解决;二是信任文件中避免混入过期或多余的CA证书,既会拉长握手时下发的证书请求列表,也可能造成意想不到的验证冲突。掌握这些细节后,你就能搭建出一套既严格又灵活的Nginx双向认证体系。
Nginxssl_verify_client客户端证书验证修改时间:2026-08-31 04:42:43