Nginx双向TLS认证实战:OpenSSL自建CA签发客户端证书

发布时间:2026/10/21 6:09:39
Nginx双向TLS认证实战:OpenSSL自建CA签发客户端证书 在局域网里跑服务最烦的就是身份认证。口令太弱容易被嗅探JWT 签名密钥一旦泄露整个系统都得重发。前几年我在团队里接了个需求一组内部 API 要开放给十来台外部服务器调用双方都是我们自己的机器不能用公网 CA 证书但又要确保“只有这十几台设备能连进来其他人一律拒绝”。折腾一圈之后我选了——PKI 体系里最朴素的那套打法用 OpenSSL 自建 CA签发客户端证书再让 Nginx 开启双向 TLS 验证。这套东西做完之后的效果是没有证书的客户端连握手都过不去有证书但不在同一 CA 链上的同样被拒。整个过程完全走证书验证不依赖密码和 IP 白名单。这篇文章就把这套“借助 Nginx 实现客户端自签证书供服务端验证”的完整链路拆开讲清楚。先解释 PKI 信任链到底是怎么搭建的再手把手带你生成 CA、签发服务端证书和客户端证书最后落到 Nginx 配置和 curl 实测。如果你是刚接触 mTLS 的小白跟着操作一遍就能跑通如果你已经配过证书但这种底层的信任模型没吃透重点看原理和排错部分能帮你少踩不少坑。1. 项目整体设计与思路拆解1.1 为什么“服务端验证客户端”比“客户端验证服务端”更难日常我们访问 HTTPS 网站是客户端校验服务端浏览器拿着服务端证书去和系统根证书库比对确认这个站点没被冒充。这叫单向 TLS。但单向 TLS 解决不了“服务端如何知道你是谁”的问题。你想一下这个场景后端接口接收请求靠什么判断调用方可信最常见的方案是 Header 里塞一个 token或者每个客户端用独立的 API Key。这俩方案都属于“共享秘密”只要在传输过程中被抓到一次或者某个客户端设备被攻破导致密钥泄露攻击者就能冒充合法调用方。更麻烦的是你没办法轻易吊销某个客户的访问权限——只能改代码、换密钥然后把所有客户端重新分发一遍。客户端证书验证mTLS就是来干掉这个问题。它跟 HTTPS 证书校验一样走 PKI 体系只是方向反过来服务端不再被动接受任意 TLS 连接而是强制要求客户端在握手阶段出示证书并把这个证书拿到自己信任的 CA 列表里去验签。证书里绑定的是客户端设备的公钥私钥只存在那台设备上网络传输过程中根本不会泄露私钥。谁泄露了私钥、谁该被收回访问权限服务端直接更新吊销列表或者移除对该 CA/证书的信任就行不用改业务代码。1.2 “自签证书”这条路到底怎么选标题里说的“客户端使用自签证书”在实际落地时分两种做法很多人一开始会混淆。第一种是客户端证书就是它自己签自己也就是 openssl req -x509 生成一个独立的自签设备证书然后把这张证书直接塞给 Nginx 的 ssl_client_certificate。这种做法的效果是Nginx 只能验证这张证书本身没被篡改但是没法验证它是不是由你签发。换句话说任何一台机器如果自己生成一张自签证书再把它装到客户端里理论上也能和服务端完成验证。因为你没法区分“这张证书是我发的”还是“这张证书是别人自签的”。所以纯设备自签只适用于实验不适合做真正的身份准入控制。第二种是搭一个自建 CA这个 CA 给自己签发根证书然后用这个根证书给每一台客户端设备签发证书。客户端拿到的证书虽然不是公网 CA 签的但从信任链角度看它是你的私有 CA 签发的而 Nginx 里只信任你的私有 CA 根证书。这样一来别人自己生成的杂乱证书在你的 Nginx 面前统统不认只有你亲手签发的证书才有效。我推荐的方案也是本文要写的就是第二种。标题里讲“自签”本质上是整套 PKI 体系“自己签”而不是客户端证书“自己给自己签”。二者一字之差安全性天壤之别。实际操作中自建 CA 的成本很低OpenSSL 三句话就能生成根证书但换来的是完整可控的信任边界这笔账很划算。2. 核心原理Nginx 双向认证背后的信任链2.1 证书、CA、签名一张图理解 PKIPKIPublic Key Infrastructure说白了就是一套“谁证明你是谁”的机制。技术实现上依赖三个东西非对称加密公钥加密私钥解密私钥签名公钥验签。证书把公钥和身份信息CN、组织、有效期等绑在一起由 CA 签名。CA被大家共同信任的第三方负责签发和吊销证书。当我们说“一张证书是合法的”意思不是这张证书本身有多难伪造而是它携带的签名字段能被某个受信任的 CA 公钥验证通过。证书信任是链式的客户端证书 → 签发它的 CA → CA 的根证书。只要链路里每一级的签名都能验上整条链就被信任。Nginx 在客户端认证时做的事情本质上就是验证这条链。它拿着 ssl_client_certificate 里配置的 CA 证书或 CA 证书集合对客户端传上来的证书做验签。验签通过了再看有效期、看用途扩展。不需要把整个 CA 的私钥放到服务端只需要 CA 的公钥即可这就是 PKI 最巧妙的地方——信任是公开的秘密只需要被签名方单独保存。2.2 Nginx 的 ssl_verify_client 机制nginx 里做双向认证核心就三个指令ssl_client_certificate指定客户端证书链的 CA 文件路径。ssl_verify_client设置是否验证客户端证书可选 on / off / optional。ssl_verify_depth验证深度默认 1表示验证到最后一级即客户端证书直接由 ssl_client_certificate 里的 CA 签发。如果你的客户端证书还有中间 CA需要加深。握手流程是这样的nginx 在 TLS 握手时发送 CertificateRequest 请求要求客户端提供证书。客户端把自己的证书链发给 nginxnginx 用 ssl_client_certificate 里的 CA 公钥逐级验证。验证通过后nginx 还会把客户端证书的 DN 信息如 /CNclient1存到变量 $ssl_client_s_dn 里后续做日志、鉴权、透传都可以用。如果验证失败TLS 握手直接中断客户端会看到一个错误提示比如 curl 会报 certificate verify failed浏览器会报“接收到的证书无效”。这比 HTTP 层的 401 更粗暴也更安全——业务逻辑根本没机会被未授权设备触达。2.3 可选校验模式 ssl_verify_client optional大多数场景下我们要的是强制校验也就是 ssl_verify_client on它要求客户端必须出示合法证书否则握手失败。但有一种情况你可能想放开一部分流量比如同一端口既要给浏览器访问浏览器没有客户端证书又要给内部服务调用持证书的客户端那就用 optional。optional 模式的意思是客户端如果有证书我就验证验证不通过不允许客户端没有证书我也放行但你可以根据变量 $ssl_client_verify 的值来判断等于 SUCCESS 时说明证书验证通过等于 NONE 时说明客户端没给证书。这种模式在网关改造期特别实用先记录证书信息再逐步收紧访问策略等全部客户端都切到证书后再把 optional 改成 on。我自己做网关时用过这个模式优势很明显不用停服迁移客户端先配好证书服务端观察一段时间的验证结果确认都 OK 以后切强制。缺点是 optional 模式下没证书的请求也能打到后端应用层安全性降低所以只建议在迁移过渡期开。3. 实操准备证书关联材料与生成流程3.1 环境准备与目录规划先用一台干净的 Linux 机器来操作CentOS、Ubuntu 都行只要装了 OpenSSL 即可。检查版本openssl version我这边用的是 OpenSSL 3.x。不同版本在生成命令上有细微差别下面都以 3.x 为准。证书文件建议放在独立目录里方便管理和维护。我习惯建一个 certs 目录里面再分 ca、server、client 三个子目录。所有证书相关的密钥生成操作都在这个目录下完成避免散落各处找不到。注意权限。CA 私钥文件必须设为 600普通用户可读不可写。Nginx 进程读取证书文件时通常用 root 启动然后降权但私钥文件权限太开放的话机器被入侵后私钥直接被人拿走整套 PKI 信任体系就废了。3.2 创建自建 CA第一步是生成 CA 的私钥我这里用 RSA 4096。用 ECC 也行但 RSA 的对老系统兼容性更好些openssl genrsa -out ca/ca.key 4096 chmod 600 ca/ca.key然后生成 CA 的自签根证书。用 -x509 参数直接一步到位不用先弄 CSR。CN 我习惯写成内部名称比如 Internal Lab CA一眼就能认出来是谁家的 CAopenssl req -new -x509 -key ca/ca.key -days 7300 -out ca/ca.crt \ -subj /CCN/OLab/CNInternal Lab Root CA注意 -days 7300也就是 20 年。根证书有效期必须比其他所有证书长否则根证书先过期整个信任链就崩了。签发客户端证书和服务端证书时有效期全部要小于根证书剩余生命期。顺手再看一眼根证书内容openssl x509 -in ca/ca.crt -noout -text重点关注 Signature Algorithm、Validity、Subject确认基本信息没问题。3.3 签发服务端证书服务端证书是给 Nginx 用的也就是客户端访问 https 时看到的证书。因为是自建 CA浏览器不会默认信任要么把 ca.crt 导入系统信任区要么测试时用 curl 的 -k 跳过校验。不过生产中最好导入信任区不然客户端工具校验会失败。先生成服务端私钥和签名请求 CSRopenssl genrsa -out server/server.key 2048 openssl req -new -key server/server.key -subj /CNmyapp.internal -out server/server.csr如果服务端需要被多个域名或 IP 访问一定要带上 SANSubject Alternative Name否则现代浏览器并不认 CN 字段。新建一个 server.ext 文件subjectAltName DNS:myapp.internal, DNS:localhost, IP:127.0.0.1然后用 CA 对 CSR 签发证书openssl x509 -req -in server/server.csr -CA ca/ca.crt -CAkey ca/ca.key \ -CAcreateserial -days 825 -out server/server.crt -extfile server.ext-days 825 是故意选的825 天的保险证书有效期是苹果和 CA/Browser 论坛推荐的上限。自签场景内你自己定也行不过用这个习惯不容易出错。签发完可以校验openssl verify -CAfile ca/ca.crt server/server.crt返回 OK 表示证书链没问题。3.4 签发客户端证书客户端证书跟服务端证书生成流程几乎一模一样区别只在 CN 和用途扩展。CN 是服务端区分“谁在调用”的主要依据我喜欢按设备或角色命名比如 client1、client-monitor-01。别用重复名否则吊销和排障时根本分不清是哪台设备。openssl genrsa -out client/client.key 2048 openssl req -new -key client/client.key -subj /CNclient1 -out client/client.csr openssl x509 -req -in client/client.csr -CA ca/ca.crt -CAkey ca/ca.key \ -CAcreateserial -days 365 -out client/client.crt需要确认客户端证书的密钥用途扩展包含 clientAuth。如果 CSR 里没带扩展x509 -req 默认可能不写入 extendedKeyUsage有的老客户端不校验这个但我建议显式加上。写一个 client.extextendedKeyUsage clientAuth签发时用 -extfile client.ext。两套都试试你就知道区别了现代服务端包括 nginx默认不强制校验这个扩展但为了规范还是加上。签发完校验openssl verify -CAfile ca/ca.crt client/client.crt3.5 打包客户端证书PKCS#12服务器交互用 client.crt 和 client.key 两个文件没问题但如果你要让同事把证书导入浏览器或者放到 Windows 机器的证书库里这两个文件不太方便。打包成 PKCS#12即 pfx是一个文件里面同时包含公钥、私钥和 CA 链openssl pkcs12 -export -out client/client.pfx \ -inkey client/client.key -in client/client.crt \ -certfile ca/ca.crt -passout pass:yourpassword导出时设置的密码要妥善保管导入到其他设备时需要用到。证书私钥本身只有这一份丢了没人能帮你找回只能重新签一张。4. Nginx 配置与联调实测4.1 nginx.conf 中的关键配置服务端证书文件就位之后开始配 Nginx。配置文件里 server 块的核心部分长这样server { listen 443 ssl; server_name myapp.internal; ssl_certificate /etc/certs/server.crt; ssl_certificate_key /etc/certs/server.key; ssl_client_certificate /etc/certs/ca.crt; ssl_verify_client on; ssl_verify_depth 1; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log warn; location / { root /usr/share/nginx/html; index index.html; } }解释几个容易踩坑的细节。第一ssl_certificate 路径和 ssl_client_certificate 路径不要搞混。前者是服务端证书发给客户端看后者是 CA 证书用来验客户端。如果写反Nginx 启动时会报错或者握手时证书链验证失败。第二ssl_client_certificate 放的是 CA 公钥绝对不能把 ca.key 放进去。我见过有人图省事把整个证书目录都配进去了还好 Nginx 只读取证书文件内容不会把私钥当证书用但目录权限一旦混乱泄私钥的风险就出来了。第三ssl_verify_depth 默认是 1。如果你的 CA 结构是 根CA → 中间CA → 客户端证书 这样的多层结构必须改成 2 或更深否则 Nginx 在证书链验到中间层就停了会报 unknown ca 错误。简单的单层 CA 结构用默认 1 就够了。配置完检查语法nginx -t通过之后重载systemctl reload nginx4.2 curl 实测证书验证通过先测正确的访问方式。客户端证书和 CA 都要带服务端证书按理说应该用 CA 验证但测试时如果尚未导入信任区可以直接用 -k 跳过服务端证书校验只验证客户端链路curl -k https://myapp.internal/ \ --cert client/client.crt \ --key client/client.key \ -v-v 能看到完整的握手日志。关注几个关键输出TLSv1.3 握手开始发送 Certificate 和 CertificateVerify 信息最终 HTTP/2 200如果你希望服务端证书也走完整验证用 --cacert 指定 CA 即可curl --cacert ca/ca.crt https://myapp.internal/ \ --cert client/client.crt \ --key client/client.key注意 --cacert 指向根证书同时客户端证书必须在 CA 链上。4.3 curl 实测没有证书会被拒再测一个反向场景。不带任何客户端证书访问curl -k https://myapp.internal/预期结果HTTP 400页面内容无所谓核心是错误信息400 No required SSL certificate was sent这个报错意味着 Nginx 在 TLS 握手阶段就拒绝了请求应用层根本没收到。从安全角度说这是理想的结果——非法请求在 TLS 层被挡掉比打回业务层再判断安全得多。用验证不通过的自签证书访问也会在握手阶段挂掉curl 报 verify errorNginx error.log 里有 ssl_client_certificate verify error说明证书链验证失败。4.4 浏览器导入证书后的访问如果客户端是浏览器直接把 client.pfx 导入系统证书库正常时必须能访问不导入就访问时会弹证书选择框或者直接给出 400。浏览器和 curl 有个区别curl 如果不指定客户端证书就直接发空浏览器则会先弹出证书选择列表让你挑一个。如果列表里没有合法证书浏览器可能什么都不说直接失败。这一点在排障时经常让人误以为服务端配置有问题其实问题在浏览器证书库。把 CA 根证书导入浏览器的信任区之后访问 https 不再报警不受信任整个过程就顺了。方法是把 ca.crt 下载到本地双击导入到“受信任的根证书颁发机构”。4.5 后端如何拿到客户端身份除了做访问控制mTLS 最大的好处是服务端能“看见”调用方的身份。Nginx 内置变量 $ssl_client_s_dn 保存了客户端证书的 DN比如/CNclient1你可以在 location 里用 header 把这个值传给后端location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header X-SSL-Client-DN $ssl_client_s_dn; }后端服务从 HTTP 头里读 X-SSL-Client-DN就能知道这次请求来自 client1不需要再传 token 或用户名。但这里有个安全细节因为 Nginx 会覆盖这个 header下游只能信任 Nginx 传来的值担心用户伪造的话可以在 Nginx 层用proxy_set_header强制覆盖或者把内部调用端口限制在本地回环确保只有 Nginx 的请求能打到后端。5. 常见问题与排查技巧实录5.1 证书链验证失败unknown ca错误日志长这样client SSL certificate verify error: self-signed certificate in certificate chain最常见的两个原因一是 ssl_client_certificate 里配的 CA 不是签发客户端证书的那张比如你把服务端证书文件当成 CA 放进去了二是 ssl_verify_depth 设置不够证书链中间有内层 CA 没验。排查方式是用 openssl verify 手动模拟 Nginx 的验证过程openssl verify -CAfile ca/ca.crt client/client.crt如果返回 unknown ca 或者 self-signed cert说明证书链建立有问题。逐级向上看客户端证书是不是由主 CA 签的签发时 -CA 和 -CAkey 有没有配对错误。我遇到过一次最诡异的签发时把 ca.crt 和 ca.key 从一个服务器拷贝到另一台结果 ca.key 和 ca.crt 不匹配所有客户端证书验签失败。这个坑很难发现因为文件都存在。提前检查openssl x509 -noout -modulus -in ca/ca.crt | openssl md5 openssl rsa -noout -modulus -in ca/ca.key | openssl md5两个 hash 必须一致。5.2 客户端发来的证书链不完整有些客户端只发送自己的证书不发中间 CA 证书。Nginx 验到中间级别找不到上级 CA报证书链中断。解决方案是在客户端导入证书时确保带上完整链或者用 openssl 合并证书链cat client.crt intermediate.crt client-chain.crt然后在 curl 里指定 client-chain.crt 作为客户端证书。一般单层 CA 不会遇到这问题但如果你为了安全性加了中间 CA这张牌必须打好。5.3 证书有效但 Nginx 直接拒绝遇到现象证书是刚签的openssl verify 也 OK可 Nginx 就是拒绝。这类问题通常是系统时间不对或者证书用途扩展不匹配。先看时间dateNginx 所在机器和生成证书的机器时间相差过大新签的证书在 Nginx 看来还没到生效时刻直接拒绝。这个坑在开发环境特别常见尤其虚拟机快照回滚之后。同步一下时间服务即可。再看证书用途openssl x509 -in client.crt -noout -ext extendedKeyUsage如果显示 TLS Web Server Authentication说明签发时把客户端证书按服务端证书用途签了。此时把 -extfile client.ext 里加上 extendedKeyUsage clientAuth 重新签一张。5.4 客户端证书被吊销有些场景下某台设备离职或者不再访问你要单独收回权限不需要重签所有证书只需要维护一张吊销列表 CRL。Nginx 用 ssl_crl 指令指向 CRL 文件openssl ca -gencrl -keyfile ca/ca.key -cert ca/ca.crt -out ca/crl.pem然后在 Nginx 配置中加上ssl_crl /etc/certs/crl.pem;添加后 reload Nginx。当客户端证书在 CRL 内时Nginx 会拒绝。不过要注意新生成的 CRL 文件必须包含之前的吊销记录做法是维护好 Nginx 的 index.txt 或者每次用同一份数据库。手工在 index.txt 里追加吊销记录比较繁琐但如果证书数量不大用脚本维护也够用。吊销的管理手段其实可以更简单粗暴直接更新 ssl_client_certificate 指向的 CA 证书集合。比如把某个客户端证书从你维护的一个目录中移除然后 reload Nginx。这种做法适合证书数量极少的场景优点是直观缺点是不灵活。证书数量一多还是老老实实用 CRL 或 OCSP。5.5 一个容易忽略的权限问题Nginx 的 worker 进程读取证书文件需要合适的权限。证书私钥是 600属主 root可 Nginx worker 以 nginx 用户运行可能在 reload 后 log 里出现 permission denied。解决办法是保持私钥属主 root 同时设置组为 nginx 或者调整权限到 640文件路径写到配置里后建议先sudo -u nginx test -r /etc/certs/server.key echo ok确认 Nginx 用户能读取再执行 reload。这个问题不常遇到但一遇到就会卡住人因为 Nginx 启动时可能没报错到握手失败时才会暴露。6. 实操过程中的几点体会整套流程走下来我觉得最容易让新手翻车的不是命令本身而是对信任模型的理解。证书能不能被验证看的不是文件里有几个字而是签发链路是否完整、时间是否有效、用途是否匹配。这些维度任何一项出问题结果都是握手失败但错误信息五花八门你没有形成“链式排查”的意识会绕很多弯。另一个经验是关于私钥备份的。CA 私钥是整个体系的命根子。丢了所有已签发的证书全部作废泄露你的 PKI 等于不存在。我习惯在生成 CA 之后同时备一份加密压缩包放到离线存储且绝不放进 Nginx 的 web 目录里。客户端证书私钥也要在签发后第一时间导出 pfx否则别人拿到证书没有私钥照样没法用。最后建议你从最小配置开始验证。先在本机配置一个只包含默认页的 server用 curl 跑通握手再加业务 location。每加一个模块问题就少一个。双向认证不像普通配置文件改完立竿见影它涉及客户端、服务端、证书三方的协作一步一步来反而比一次梭哈改到半夜更省时间。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询