Nginx 开了双向 TLS,却出现 400 No required SSL certificate was sent。换 fullchain 也没用:被检查的可能是客户端。下面用 curl 区分"没带证书""带了但不可信""验错信任方向",不靠关闭验证掩盖故障。

一、先分清是谁在验证谁
普通 HTTPS 中,客户端验证服务器;mTLS 再增加服务器验证客户端。两条信任链不是同一个开关。curl --cacert 指定客户端信任的服务端 CA;--cert 与 --key 指定客户端证书和本地私钥(私钥不发送给服务器);Nginx 的 ssl_client_certificate 指向验证客户端证书的 CA 文件。
图中命令行参数属于 curl;"服务器身份"指客户端验证服务器的信任方向。CA 包不会凭空生成客户端身份,就像大楼照片不能当工牌。本文只讨论客户端到 Nginx 的一段,先确认谁终止 TLS。
二、这句 400 与 495、496 是什么关系
Nginx 官方文档区分两类内部错误:496 表示没有提交必需的客户端证书,495 表示客户端证书验证错误,均可供 error_page 处理。它们不等于浏览器默认一定收到的 HTTP 状态。
这次我在隔离回环环境使用 Nginx 1.26.3、curl 7.61.1,固定 TLS 1.2 做对照:必需证书缺失时,对外是 HTTP 400 和上述英文提示;换成另一 CA 签发的客户端证书,则是 HTTP 400 与 The SSL certificate error。不同版本、TLS 后端或网关可能表现为握手失败,不要只按浏览器文案下结论。
同时记录 HTTP、正文和退出码。不加 --fail 时,HTTP 400 仍可能对应 curl 退出 0;HTTP 000 表示没收到 HTTP 响应,不是服务器状态码。
三、先用最小配置确认验证边界
下面是实验用 server 片段,不可覆盖生产配置。准备服务端 fullchain、匹配私钥和客户端 CA 文件;路径为示意,8443 仅绑定回环。
nginx
server {
listen 127.0.0.1:8443 ssl;
server_name mtls.test;
ssl_certificate /path/to/server-fullchain.pem;
ssl_certificate_key /path/to/server.key;
ssl_client_certificate /path/to/client-ca.pem;
ssl_verify_client on;
ssl_verify_depth 1;
location / {
return 200 "verify=$ssl_client_verify\n";
}
}
ssl_verify_depth 默认是 1,应结合实际中间证书链和版本验证,不是越大越保险,更不能修复信任了错误 CA 的问题。这里仅测试根 CA 直接签发的叶子证书,未验证多级中间 CA 的深度边界。诊断响应只用于回环实验,生产环境不对外暴露详细验证原因。
四、同一目标做三次 curl 对照
准备 PEM 格式的 client.crt、client.key 和 server-ca.crt。假定已启动 8443 实验监听,服务端 SAN 包含 mtls.test。
bash
curl --noproxy '*' --http1.1 \
--tlsv1.2 --tls-max 1.2 \
--resolve mtls.test:8443:127.0.0.1 \
--cacert server-ca.crt \
--cert client.crt --key client.key \
--include --write-out '\nhttp=%{http_code}\n' \
https://mtls.test:8443/
第一次删掉 --cert/--key,看无身份对照;第二次带可信客户端证书;第三次换成另一 CA 签发的测试客户端证书。第三次不要改 --cacert,否则同时改变另一条信任链,排查又绕回去了。
--resolve 只替换连接地址,URL 保留主机名供 SNI 和名称校验使用;IP URL 加 Host 头并不等价。固定 TLS 1.2 是为了实验结果可比,两个 TLS 参数要一起理解:单独 --tlsv1.2 表示最低版本,不是最高版本。不要把实验限制照搬成生产策略。
五、optional 并不是"少检查一点也没事"
以下是本次隔离测试的真实结果。每个模式都更换三类客户端输入,保持服务端信任配置不变。
| 模式 | 不带客户端证书 | 可信证书 | 另一 CA 证书 |
|---|---|---|---|
| on | 400,缺少证书 | 200,SUCCESS | 400,证书错误 |
| optional | 200,NONE | 200,SUCCESS | 400,证书错误 |
| optional_no_ca | 200,NONE | 200,SUCCESS | 200,但 FAILED |
optional 允许不提交,已经提交的证书仍要验证;optional_no_ca 不要求证书由受信 CA 签发,官方用途是把实际验证交给外部服务,不是万能跳过所有错误。没有后续验证与授权,直接改这个开关,等于把门禁故障修成了门常开。
HTTP 200 同时伴随 FAILED:unable to verify the first certificate,直接说明应用成功不等于 mTLS 通过。
六、带了证书还失败,检查这几处
先查有效期、签发者和用途,再查链。文件名叫 client.crt 不代表它一定适合客户端认证;若证书限制了扩展用途,要确认包含 clientAuth。以下只读取公开证书,不打印私钥。
bash
openssl x509 -in client.crt -noout -subject -issuer -dates
openssl x509 -in client.crt -noout -purpose
openssl verify -purpose sslclient \
-CAfile client-ca.pem client.crt
若实际使用中间 CA,离线验证需要通过 -untrusted 提供对应中间证书;客户端发送的链也应齐全,不能靠只信任错误根来补救。检查 Nginx 实际读取的 CA 文件、文件权限及客户端证书与私钥是否配对。不要把私钥上传在线工具,也不要把完整证书、身份信息和调试日志随手贴到工单。
反向对照只换错 server-ca.crt,本次得到 curl 退出 60、HTTP 000。这是客户端不信任服务器,两个方向要分别修。
七、续期工具解决不了客户端授权
服务端证书更新与客户端身份管理要分开安排。中立比较三类资料与工具:Let's Encrypt 官方文档用于核对 ACME 签发与验证规则;acme.sh提供脚本化证书申请与安装流程;CertbotX提供平台化服务端证书生命周期管理。这里比较的是运维层次,不是客户端 PKI 或权限系统的替代方案。
服务端续期不会自动让客户端进入可信列表;认证通过也不等于允许访问任意租户。账号映射、资源权限、吊销与审计需另设规则。网关不能相信外部随意传来的"客户端身份"请求头。
八、验收:拒绝该拒绝的,也要写进测试
- 固定目标域名、SNI、端口与 TLS 终止节点。
- 不带证书应按策略拒绝,正确身份才能通过。
- 错误 CA 的客户端证书不能被误放行。
- 分别核对客户端与服务端的信任链。
- 同时检查 HTTP、curl 退出码和 ssl_client_verify。
- 证书认证后继续执行业务授权,不用 -k 或关闭验证掩盖问题。
技术依据:Nginx SSL 模块与curl 官方手册。本次实验仅使用临时 CA 和回环监听,测试后清理私钥及独立进程;没有修改生产配置或系统信任库。