未来星球 未来星球
  • 首页
  • 新闻资讯
  • 技术教程
  • 软件工具
  • 知识百科
  • 生活实用
  • 个人随笔
  • 生命外星人
首页 › 技术教程 › 用Nginx做双向认证:3份配置,和4个必然踩到的坑
  • 0
  • 0

用Nginx做双向认证:3份配置,和4个必然踩到的坑

admin
22 小时前
8 views
用Nginx做双向认证:3份配置,和4个必然踩到的坑-未来星球

给一台内网 API 加访问控制,最省事的做法是发一串密钥,写进请求头。三个月后你会发现密钥被复制到了三个仓库、两个聊天窗口和一份离职同事的笔记里。双向 TLS 的价值不在于它更难破解,而在于它把「凭据」变成了一件不能靠复制传播的东西。

下面这套配置在本机 nginx 1.26.2(stable)配 OpenSSL 3.0.13 上验过,Debian 12 与 Ubuntu 24.04 的软件源版本可直接照抄。先跑一次 nginx -V 确认你的编译参数里有 --with-http_ssl_module。版本差异主要落在证书校验的错误码上,配置语法这两年没变。

一、先把目录和角色分清楚

双向认证里一共有三个角色:一个签发者(CA),一个服务端,若干个客户端。签发者的私钥是整套体系的根,它签谁谁就能进门。CA 私钥一旦泄露,整套证书全部作废,所以它不该和 nginx 放在同一台机器上。

/etc/nginx/tls/
├── ca.crt                # 根证书,公开,客户端要拿它验服务端
├── server.crt            # 服务端证书
├── server.key            # 服务端私钥,权限 600
├── client-ca-bundle.crt  # nginx 用的信任库:根 CA + 中间 CA
└── crl.pem               # 可选,吊销列表

/root/ca/                 # 只在这一步用,签完就离线
├── ca.key                # 根私钥,权限 600
└── ca.srl                # 序列号文件,务必保留

把签发用的私钥和线上用的证书分开存,是为了让「谁能发证」和「谁能用证」变成两件事。很多内网事故不是证书被盗,是签发能力被顺手复制走了。

二、建一个只干一件事的 CA

下面这条命令生成一个十年有效、4096 位的自签根证书。-nodes 表示私钥不加密码,适合自动化场景;如果你打算长期离线保存,去掉它并设一个密码更稳妥。-nodes 省下的是每天输一次密码的时间,赌上的是这台机器本身的边界。

mkdir -p /root/ca && cd /root/ca

openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
  -keyout ca.key -out ca.crt \
  -subj "/C=CN/O=fhsj-internal/CN=fhsj-internal-ca" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

chmod 600 ca.key

pathlen:0 这一行限制了这张根证书只能签终端证书,不能再签出二级 CA。少一层中间 CA,就少一个将来没人说得清来历的信任来源。

三、签服务端与客户端证书

服务端证书的 CN 写域名,但真正被校验的是 SAN(Subject Alternative Name)。现代客户端已经不看 CN 了,浏览器、curl、Go 都一样。只写 CN 不写 SAN 的证书,会在握手完成后被客户端一句「名称不匹配」直接丢掉。

cd /root/ca

# 服务端:注意 SAN 里有 DNS 也有 IP
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \
  -subj "/CN=api.internal"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAserial ca.srl \
  -days 825 -sha256 -out server.crt \
  -extfile <(printf "subjectAltName=DNS:api.internal,IP:10.0.8.11\nextendedKeyUsage=serverAuth\nkeyUsage=digitalSignature,keyEncipherment\n")

# 客户端:每一个设备一张,CN 用来区分身份
openssl req -newkey rsa:2048 -nodes -keyout device-001.key -out device-001.csr \
  -subj "/CN=device-001"
openssl x509 -req -in device-001.csr -CA ca.crt -CAkey ca.key -CAserial ca.srl \
  -days 365 -sha256 -out device-001.crt \
  -extfile <(printf "extendedKeyUsage=clientAuth\nkeyUsage=digitalSignature\n")

客户端证书里 extendedKeyUsage=clientAuth 这一行不能省。缺了它,Java 的 HttpClient 和部分版本的 Chrome 会拒绝使用这张证书,报的错还和证书无关,查起来很费时间。证书的用途不是注释,是握手时的硬性约束。

首次执行时 ca.srl 不存在,openssl 会报错。手工创建一次即可:echo 1000 > ca.srl。不要用 -CAcreateserial 让它自动生成,原因在后面的坑里。

四、nginx 侧的完整配置

核心只有四行:告诉 nginx 用哪张证书、用哪个信任库、要不要强制校验、校验到第几层。把这个文件当成唯一真源,不要在其他 include 里再写一遍同名指令。

server {
    listen 443 ssl;
    http2 on;
    server_name api.internal;

    ssl_certificate         /etc/nginx/tls/server.crt;
    ssl_certificate_key     /etc/nginx/tls/server.key;
    ssl_trusted_certificate /etc/nginx/tls/client-ca-bundle.crt;

    # 信任库:客户端证书必须由这里的某个 CA 签发
    ssl_client_certificate  /etc/nginx/tls/client-ca-bundle.crt;
    ssl_verify_client       on;
    ssl_verify_depth        2;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:MTLS:10m;
    ssl_session_timeout 10m;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-Client-Verify $ssl_client_verify;
        proxy_set_header X-Client-DN     $ssl_client_s_dn;
        proxy_set_header X-Client-Serial $ssl_client_serial;
        proxy_set_header X-Real-IP       $remote_addr;
    }
}

ssl_verify_depth 2 的意思是:客户端证书之后最多再往上两级(中间 CA、根 CA)。只放根 CA 的信任库配这个值,链稍长就会校验失败。深度不是越大越好,它决定了你能接受多长的证书链,也决定了攻击面。

后端拿到 X-Client-DN 就能知道是谁,比如 /CN=device-001。这个头只能由 nginx 注入,应用侧必须拒绝外部直接传入的同名头。否则整套双向认证会被一个请求头绕过,这是最常见的整体失效方式。

五、验收:三个正向,两个负向

正向第一条,带证书访问应该拿到 200,并把 DN 回显出来。这条只能证明「进得去」,证明不了「进的是对的门」。

curl -sS --cert device-001.crt --key device-001.key \
     --cacert ca.crt https://api.internal/health -v 2>&1 | grep -E "SSL certificate|HTTP/"

正向第二条,用 openssl s_client 看握手细节,重点确认服务端确实发出了 CertificateRequest、客户端证书被接受。curl 只告诉你结果,s_client 才告诉你握手到底走到哪一步。

openssl s_client -connect api.internal:443 -state \
  -cert device-001.crt -key device-001.key -CAfile ca.crt </dev/null 2>&1 \
  | grep -E "Acceptable client certificate CA names|Verify return code"

负向测试更能说明问题。不带证书应当被拒,带一张别家 CA 签的证书也应当被拒。两条都要手工跑一遍,因为「不带证书被拒」可能只是防火墙挡住了,和被拒长得一样。能区分「拒绝的原因」和「拒绝的结果」,才算真的验过。

六、四个必然踩到的坑

第一个坑是序列号。用 -CAcreateserial 签第二张证书时,openssl 会重新生成 ca.srl 并从 1 开始编号,于是两张不同设备的证书拿到了同一个序列号。序列号是吊销和审计的唯一依据,重复意味着吊销一张等于吊销一批。

第二个坑是信任链。很多教程让你把中间 CA 也塞进 ssl_client_certificate,理由是「客户端的链可能不全」;但更常见的失败是反过来的:信任库里只放了根 CA,而客户端只发送自己的证书、不发送中间证书,nginx 走到一半找不到签发者,返回 400,日志里只有一行 client sent no required SSL certificate,和真实原因完全不搭边。解决办法是把中间 CA 追加进信任库文件,而不是把 ssl_verify_depth 调大。

第三个坑是按路径区分。想把 /public 放开、其余强制校验,直觉上会在 location 里写 ssl_verify_client optional。这在 HTTP/1.1 下勉强能用,在 HTTP/2 下必然出问题,因为 h2 禁止 TLS 重协商,一次连接只握手一次。要按路径分权,正确做法是拆成两个 server 块或两个端口,而不是在 location 里做条件判断。

第四个坑最隐蔽:撤销不等于断开。mTLS 只在握手阶段验证一次,之后的长连接不再检查证书状态。一张证书在 10 点被吊销,9 点建立的连接可以一直用到它自己断开。把 ssl_session_timeout 从默认的 5 小时压到 10 分钟,只能缩小窗口,不能消除它。

要真正兜住这一点,得在应用层做两件事:校验 X-Client-Serial 是否在有效清单里,以及给长连接设一个绝对生存期。传输层的身份和工作层的授权,是两件不能互相替代的事。

写在最后

双向认证最容易被误解的地方,是把它当成一道门锁。它更像一张门禁卡:卡本身不能复制粘贴,丢了要正式注销,谁在什么时候刷过都有记录。它真正的收益不是拦住陌生人,是让「谁进来过」这件事第一次变得可回答。

代价也很清楚:每台设备一张证书、每个季度一次轮换、每次轮换一次发版。这些工作量不会因为自动化消失,只会因为自动化变得可以计算。一个安全方案能不能长期活着,取决于它的运维成本,而不是它的强度。

所以真正该问的不是「要不要上 mTLS」,而是「有没有一个地方,值得为它维护一套签发流程」。如果答案是「只有一个后台」,那 API key 加 IP 白名单就够了;如果答案是「几十台设备、每年换人」,那密钥方案迟早会以一次泄露结束。


你手上那套内网服务,现在是用什么方式确认「对面是谁」的?换过证书之后,有没有遇到过只报「400」不报原因的握手失败?

0

键盘为什么不是ABC顺序:1873年的一次试错,和一个误传的动机

上一篇

本地找文件:4个工具的边界,和3种选错的信号

下一篇

评论 (0)

登录后可发布~
立即登录
暂无评论

admin

当生活给你的全是不如意,那你一定要相信自己的兴趣!
153
文章
0
评论
-11
喜欢
2026 年 11 月
一 二 三 四 五 六 日
 1
2345678
9101112131415
16171819202122
23242526272829
30  
« 10 月    

近期评论

聚合文章

当护工变成一台机器:三代人的照护,和一个不太好接受的推论
木卫二的冰下海洋:100公里的推断,和3次没测到的证据
楼道灯坏了三个月:一个灯泡,两次爬梯子,和墙上那道磨痕
AI新闻日报 – 2026年11月1日:175万亿次,和11小时的失守
家里水压突然变小:一份按收益排序的排查清单
本地找文件:4个工具的边界,和3种选错的信号
用Nginx做双向认证:3份配置,和4个必然踩到的坑
键盘为什么不是ABC顺序:1873年的一次试错,和一个误传的动机

猜你喜欢

用Postgres 16逻辑复制做零停机迁移:四个必踩的坑

用Postgres 16逻辑复制做零停机迁移:四个必踩的坑

admin
8 0 0
深入理解 JavaScript 异步编程:从回调到 async/await

深入理解 JavaScript 异步编程:从回调到 async/await

admin
94 0 0
2026 年必学的 5 个 AI 提示词技巧

2026 年必学的 5 个 AI 提示词技巧

admin
97 0 0
2026年AI工具实战指南:7个免费工具让你效率翻倍

2026年AI工具实战指南:7个免费工具让你效率翻倍

admin
86 0 0

关于我们

你是否对浩瀚的宇宙充满好奇?你是否渴望掌握未来科技的力量?现在,机会来了!“未来星球”将带你进入奇妙世界,开启你的科技探险之旅! 这是一个充满想象和创造力的博客!

分类导航

首页 新闻资讯 技术教程 软件工具 知识百科 生活实用 个人随笔 生命外星人

友情链接

未来星球AI工具【网络版】 未来星球 pycharm官方站【中国】
Copyright © 2026 未来星球. Designed by nicetheme. 蒙ICP备2026002569号-1
  • 请到 [后台->外观->菜单] 中设置菜单
  • AI未来
  • 生活百科
  • 档案
  • 外星人
  • 未来星球

admin

当生活给你的全是不如意,那你一定要相信自己的兴趣!
153
文章
0
评论
-11
喜欢