简介:ZXCA自信数字证书制作工具面向个人用户和小型组织,聚焦数字证书的自行制作、签发与管理,可用于数据加密、身份验证、代码签名及文件保护等场景。压缩包共8个文件,约4.84MB,以4个txt说明文档、2个exe程序、1个chm帮助手册和1个html页面组成,程序与文档相互配合,便于边操作边查阅。已有628人学习下载,说明其对证书入门与轻量级CA实践具有一定参考价值。内容覆盖证书申请、签发撤销、导入导出,以及利用公钥加密文件和代码签名等环节,并附带文件安全保护组件,可帮助读者理解证书从生成到使用的完整流程,适合需要本地搭建小型证书环境或补强信息安全基础的用户参考。
1. 从一条浏览器红叉说起:ZXCA 把自签证书的门槛压到了哪
本地把服务从 http 挪到 https,地址栏立刻一个红叉,点开详情写着NET::ERR_CERT_AUTHORITY_INVALID;后端那边更直接,日志里蹦一句PKIX path building failed。这类报错十有八九不是代码写错了,而是证书本身的问题——要么是自签的没人认,要么是域名和证书里的名字对不上,要么是链没发全。
ZXCA 这类自签名数字证书制作工具干的就是这件事:把openssl那一长串req、x509、pkcs12、keytool命令收进一个向导,填域名、选算法、定有效期,点几下出一整套能在本地和内网跑通的证书。它适合本地开发联调、内网服务上线前压测、测试环境做双向认证这几类场景,不适合对外提供公共服务的生产站点——那种必须走公共信任的签发渠道。第一次上手前,有几个概念不弄明白,后面填表基本靠猜。
2. 证书文件不是一种东西:ZXCA 产出的几类文件与各自去向
很多人第一次拿到的是一堆扩展名各异的文件——.crt、.key、.pem、.pfx、.p12、.jks、.p7b,看着都像证书,其实职责完全不同。搞混一次,后面能排查半天。所以先别急着点生成,把「谁签谁、谁装哪、谁带私钥」这三件事捋清楚,比多跑十遍向导都管用。
2.1 根 CA、中间 CA 与站点证书的三层关系
自签名体系里最容易被忽略的是层次。一个完整的、能被客户端验证通过的链,通常长这样:最上面是根 CA 证书(自签,签发者和主体是同一个名字,BasicConstraints标CA:TRUE),中间可以有一层中间 CA,最下面是站点证书(也叫叶子证书,BasicConstraints是CA:FALSE)。
验证的方向是从下往上:客户端拿到站点证书,用中间 CA 的公钥验它的签名,再用根 CA 的公钥验中间 CA 的签名,最后看根 CA 是否在自己的信任库里。任何一环缺失,验证都会断。这就是那句unable to get local issuer certificate的来源——不是证书错了,是链断了。
常见做法是简化成两层:一个根 CA 直接签站点证书。内网、本地联调、CI 环境完全够用,少一层就少一个出错点。只有当你需要给不同团队、不同环境签发不同的子 CA,或者要模拟真实签发机构的层级时,才值得多建一层中间 CA。ZXCA 的向导里通常这两种模式都有,界面怎么摆不影响字段含义,关键是搞清楚你选的到底是「谁签谁」。
还有一个概念必须提前说清楚:根 CA 的私钥是整个体系里最值钱的东西。它能签出任何域名、任何用途的证书,一旦泄露,意味着你的信任链彻底失控。所以根 CA 私钥一般不上生产服务器,离线放着,只在签发时用一下。站点证书的私钥则必须部署到对应的服务上。这两把钥匙的保管策略完全不同,别混在一个目录里。
2.2 各类输出格式与它们的真实去向
同一个证书可以有很多种「外壳」,选错外壳的典型症状是「证书明明生成了,服务却读不进去」。下面这张表是我自己常用的对照,按去处记比按格式记更快。
| 格式 | 典型扩展名 | 是否含私钥 | 常见去处 |
|---|---|---|---|
| PEM | .pem.crt.key | 分开存放 | Nginx、Node、curl、绝大多数 Linux 服务 |
| DER | .cer.der | 否 | Windows 导入、部分设备固件 |
| PKCS#12 | .pfx.p12 | 是(密码保护) | Windows 证书管理器、IIS、Tomcat |
| JKS | .jks | 是(密码保护) | Java 应用、keytool 体系 |
| PKCS#7 | .p7b | 否(可含整链) | 只需要分发证书链、不含私钥的场景 |
PEM 是最通用的一种,本质是 Base64 编码的文本,用文本编辑器打开能看到-----BEGIN CERTIFICATE-----这样的头尾标记。它的好处是可读、可拼接——你想把站点证书和中间 CA 拼成一条链,直接用cat就行。Nginx 的ssl_certificate指令接受的正是这种拼接后的文件,顺序必须是「站点证书在前,中间证书在后」,顺序反了服务能起来但握手失败。
PKCS#12 和 JKS 的区别经常被搞混。两者都是「证书 + 私钥 + 链」打包在一起,区别在于 JKS 是 Java 特有的老格式,PKCS#12 是跨平台标准。新版本的 JDK 其实已经默认支持 PKCS#12 作为 keystore 类型,如果没什么历史包袱,优先用 PKCS#12 更省事。但如果你的应用配置里写死了type="JKS",那还是得导出 JKS,否则启动时会报Keystore was tampered with, or password was incorrect——这个报错文案很有迷惑性,它大概率不是密码错了,而是格式对不上,keytool 解不开。
提示:导出的密码和 keystore 的密码是两个概念。PKCS#12 有「存储密码」和「密钥密码」两层,很多工具默认让它们相同,一旦设成不同,某个客户端只输一次密码就会失败。
2.3 有效期与密钥算法怎么定
密钥算法就两类在实战中常用:RSA 和 ECDSA。RSA 2048 位是下限,兼容性最好,老旧客户端、嵌入式设备、某些 Java 版本都认;RSA 3072 或 4096 安全余量更大,握手时多花一点点 CPU。ECDSA 用 P-256 曲线的话,密钥短、握手快,但对客户端有要求——很老的 Windows 和部分 JDK 8 早期版本不认。
我的习惯是:面向未知客户端的,一律 RSA 2048;只在自己的服务之间通信、客户端版本可控的,用 ECDSA P-256。别为了「更安全」无脑上 RSA 4096,内网环境下握手开销带来的收益远不如把证书链配对上重要。
有效期是另一个高频误区。公共信任体系里,站点证书的有效期被压缩到 398 天以内,这是行业规则。但私有 CA 签发的证书通常不受这条约束,你可以签十年。问题在于:签得越长,出问题时的暴露窗口越长,而且密钥轮换这件事会被无限期拖延。我的做法是根 CA 十年、站点证书一年,配合到期检查脚本,提前三十天告警。
还有一点,SAN(Subject Alternative Name)字段必须写全。现代浏览器早就不看 CN 字段了,只认 SAN 里的 DNS 和 IP 条目。你给192.168.1.20签证书,SAN 里就得有IP:192.168.1.20;只写了DNS:localhost,浏览器照样红叉。这个坑我见过太多次,第 5 章会专门拆。
3. 上手生成:根 CA、站点证书与 SAN 的完整配置路径
向导的价值在于把参数收敛成几个必填项,但填之前得知道自己要什么。这一章按「先列清单、再走向导、最后命令行复核」的顺序来,每一步都能单独验证,出问题容易定位。
3.1 生成前的三张清单
第一张清单是「访问入口」。把你打算用 https 访问的所有地址写下来,包括域名、主机名、IP,一个都别漏。比如本地开发可能同时用localhost、127.0.0.1、dev.internal、192.168.1.20四种方式访问同一个服务,那 SAN 里就要把这四个全写进去。漏一个,用那个地址访问时就红叉。
第二张清单是「客户端」。谁去连这个服务?浏览器、Java 后端、Node 脚本、curl、移动端 App,每个客户端的信任库位置都不一样。这一步决定了你最终要导出哪几种格式,以及根 CA 要装到哪些地方去。
第三张清单是「有效期与算法」。按上一节的建议定,别临时改。有效期这东西改一次就要重签全部证书,早定早省事。
# 生成前先把要覆盖的地址整理成变量,后面复用,避免手抖写错 DOMAIN="dev.internal" ALT_DNS="DNS:dev.internal,DNS:localhost" ALT_IP="IP:127.0.0.1,IP:192.168.1.20" DAYS=365 KEY_ALGO="rsa:2048"这段没什么技术含量,但它能救你。手工在向导里敲 SAN 的时候,多一个空格、少一个DNS:前缀,生成出来的证书就是无效条目,而且不报错。用变量拼好再粘贴,比手敲可靠得多。ALT_DNS和ALT_IP分开存是因为有些工具要求两类条目分开填,有些要求合并成一串,看界面来。
3.2 走一遍根 CA 与站点证书的生成
向导流程一般是固定套路,字段名可能不同,但语义一致。第一步建根 CA:填一个组织名(随便写,内网用不影响功能)、选算法、定有效期为十年、BasicConstraints一定要勾上 CA。生成后会得到两个文件,一个是根证书(公开,到处分发),一个是根私钥(保密,离线存)。
第二步建站点证书:填 Common Name(写主域名,虽然浏览器不看它了,但很多工具仍会读)、把上一节整理的 SAN 条目填进去、选ExtendedKeyUsage为serverAuth(如果是双向认证还要加clientAuth)、有效期一年、签发者选刚才那个根 CA。
第三步导出。至少导两份:一份 PEM 给 Nginx 或 Node,一份 PKCS#12 或 JKS 给 Java 和 Windows。导出时那个「包含完整证书链」的选项一定要勾上,不勾的话客户端拿到的就是裸的站点证书,验证时找不到签发者。
如果不依赖工具,等价的手工流程是这样:
# 1. 根 CA 私钥与自签证书,-nodes 表示私钥不加密(内网可接受) openssl genrsa -out rootCA.key 4096 openssl req -x509 -new -key rootCA.key -sha256 -days 3650 \ -subj "/CN=Internal Root CA/O=Dev" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -out rootCA.crt # 2. 站点私钥与 CSR,CSR 里带上 SAN openssl genrsa -out server.key 2048 openssl req -new -key server.key -subj "/CN=dev.internal/O=Dev" \ -addext "subjectAltName=DNS:dev.internal,DNS:localhost,IP:127.0.0.1,IP:192.168.1.20" \ -out server.csr # 3. 用根 CA 签发,扩展必须重新声明,否则 SAN 会丢 openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key \ -CAcreateserial -days 365 -sha256 \ -extfile <(printf "subjectAltName=DNS:dev.internal,DNS:localhost,IP:127.0.0.1,IP:192.168.1.20\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth") \ -out server.crt这三段命令对应向导里的三个页面,值得逐段看。第一段里-addext是给根证书加扩展,CA:TRUE和keyCertSign是不可省的,缺了它签出来的证书链在验证时会直接被拒,报invalid CA certificate。第二段生成 CSR,subjectAltName写在这里只是「申请」,最终以签发时的为准。
第三段是坑最多的地方。用openssl x509 -req签发时,不会自动继承 CSR 里的扩展,你必须用-extfile重新声明一遍。很多人第一步写了 SAN,第三步忘了写,最后证书里干干净净什么都没有,浏览器红叉,还以为是签发失败。这是手工流程里最经典的翻车点,也是 ZXCA 这类工具最有价值的地方——它把扩展继承这件事在内部处理掉了。
3.3 用命令行把生成结果验一遍
不管用向导还是命令行,出证书后一定要验。别信界面上的「生成成功」,那是文件写出成功,不是证书有效。
# 看证书主体、签发者、有效期和所有扩展 openssl x509 -in server.crt -noout -text # 检查 SAN 是否齐全,这一行输出就是最终生效的地址列表 openssl x509 -in server.crt -noout -ext subjectAltName # 确认私钥和证书是一对,输出两个公钥哈希一致才算配对 openssl x509 -in server.crt -noout -pubkey | openssl sha256 openssl pkey -in server.key -pubout | openssl sha256 # 验证链是否完整,必须输出 "OK" openssl verify -CAfile rootCA.crt server.crt四条命令对应四个必须确认的点。第一条看整体,重点看X509v3 extensions那一段有没有 SAN、KeyUsage、ExtendedKeyUsage。第二条单独把 SAN 拎出来,输出里应该列出你填的每一个域名和 IP,少一个都不行。
第三条是配对检查,输出两行哈希必须完全相同。如果不同,说明这个私钥不是这张证书的,常见于从别处拷了证书但私钥忘了换,或者向导里选错了密钥文件。这个错误在服务启动时报的是key values mismatch,看着像配置问题,其实是文件问题。
第四条验证链。如果报unable to get issuer certificate,说明根证书没参与验证或者站点的签发者不是这个根;如果报certificate has expired,看有效期;如果报error 20 at 0 depth lookup,多半是根证书的BasicConstraints没设对。这一条能通过,基本就说明证书本身没毛病了。
4. 服务端接入:Nginx、Java、Node 三处的参数对照与验证命令
证书生成对了,下一关是让服务读进去。三种技术栈的接入方式差异挺大,各自有各自的「卡点」,这里按配置最短到最长的顺序排。
4.1 Nginx:证书链拼接与 ssl_trusted_certificate
Nginx 只认 PEM,而且要求ssl_certificate指向的文件里是「站点证书 + 中间证书」的拼接结果,站点证书必须在前面。如果你用的是两层结构(根直接签站点),那这个文件里可以只有站点证书,但客户端必须自己装根证书,否则仍然报错。
server { listen 443 ssl; server_name dev.internal; # 必须是 fullchain:站点证书在前,中间证书在后 ssl_certificate /etc/nginx/certs/server.fullchain.pem; ssl_certificate_key /etc/nginx/certs/server.key; # 双向认证时用,普通单向认证可以不配 ssl_client_certificate /etc/nginx/certs/rootCA.crt; ssl_verify_client off; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; }ssl_certificate和ssl_certificate_key是最基本的两个,配对错了 Nginx 直接起不来,nginx -t会报SSL_CTX_use_PrivateKey之类的错。ssl_client_certificate只有在开启双向认证时才需要,单向认证配了也不影响,但会让人误以为开了 mTLS。
ssl_protocols建议只留 TLSv1.2 和 TLSv1.3,TLSv1.0/1.1 早就该关掉了,留着只会让安全扫描报告难看。ssl_ciphers那一行是常用起手式,!aNULL排除匿名套件,!MD5排除弱哈希,具体取值按你的客户端兼容性再调。
拼接证书链用一条命令就够了:
# 顺序不能反:站点证书 → 中间证书 → (可选)根证书 cat server.crt intermediate.crt > server.fullchain.pem # 改完配置先验证语法,再 reload,别直接 restart nginx -t && nginx -s reload拼接顺序反了是很容易犯的错,因为文件内容都是BEGIN CERTIFICATE开头,肉眼看不出来。反了的症状是:openssl s_client能连上,日志里却报unable to verify the first certificate。根证书要不要拼进去?一般不用,客户端本来就应该有根证书,拼进去反而让每次握手多传几 KB。
注意:
nginx -s reload之后如果客户端仍报旧错误,先确认浏览器有没有缓存 TLS 会话,换个无痕窗口或者openssl s_client直接测,别对着浏览器反复刷。
4.2 Java:JKS 与 PKCS12 的别名和密码
Java 这边是坑最密集的。核心概念只有两个:keystore 是「自己的证书和私钥」,truststore 是「我信任谁」。服务端配 keystore,客户端配 truststore,两件事别搞反。
# 把 PEM 证书和私钥导成 PKCS12,注意 -name 就是别名,后面要引用 openssl pkcs12 -export \ -in server.crt -inkey server.key -certfile rootCA.crt \ -name dev-internal -out server.p12 \ -passout pass:changeit # 转成 JKS(老应用需要),别名沿用上面那个 keytool -importkeystore \ -srckeystore server.p12 -srcstoretype PKCS12 -srcstorepass changeit \ -destkeystore server.jks -deststoretype JKS -deststorepass changeit \ -destkeypass changeit # 客户端侧:把根 CA 导入信任库,这是 PKIX 报错的解药 keytool -importcert -noprompt -alias internal-root \ -file rootCA.crt -keystore truststore.jks -storepass changeit-name dev-internal这个别名不是随便起的,Java 应用配置里如果指定了server.ssl.key-alias,就必须和它一致,不一致启动时报Alias name [xxx] does not identify a key entry。-certfile参数很关键,它决定了导出的 p12 里带不带证书链;不写的话,导出来的文件只含站点证书,客户端验证时照样断链。
第二条命令是把 PKCS12 转成 JKS,-destkeypass和-deststorepass在 JKS 里必须是同一个值,设成不同会直接报错,这是 JKS 格式本身的限制,不是工具的问题。新版本 JDK 其实可以直接用 PKCS12,如果你的应用没有历史包袱,跳过这一步更省事。
第三条是客户端侧最关键的一步。Java 的默认信任库在$JAVA_HOME/lib/security/cacerts里,里面全是公共 CA,没有你的内网根。所以要么把你的根 CA 导进去(需要改全局文件,不建议),要么单独做一个 truststore 并通过启动参数指定:
# 让 JVM 使用独立信任库,而不是动全局的 cacerts java -Djavax.net.ssl.trustStore=/opt/certs/truststore.jks \ -Djavax.net.ssl.trustStorePassword=changeit \ -jar app.jar这两个参数是排查PKIX path building failed的第一手段。很多人导入了根证书但还是报错,八成是因为导入到了cacerts,而应用启动参数里又指定了别的 truststore,等于白导。排查时打印一下System.getProperty("javax.net.ssl.trustStore"),看实际生效的是哪个文件。
4.3 Node 与 curl:把根 CA 喂给运行时
Node 的https模块默认也只认公共 CA。给它指定私有根证书有两种方式,全局的用环境变量,局部的用请求参数。
const https = require('https'); const fs = require('fs'); // ca 选项接受单个证书或数组,这里放根 CA,不是站点证书 const options = { hostname: 'dev.internal', port: 443, path: '/health', method: 'GET', ca: [fs.readFileSync('/opt/certs/rootCA.crt')], // servername 必须写,SNI 和证书校验都依赖它 servername: 'dev.internal', }; https.get(options, (res) => { console.log('status:', res.statusCode); res.on('data', (d) => process.stdout.write(d)); }).on('error', (err) => { // 常见的是 UNABLE_TO_VERIFY_LEAF_SIGNATURE 或 SELF_SIGNED_CERT_IN_CHAIN console.error('握手失败:', err.code, err.message); });ca里放的是签发者的根证书,不是站点证书,放错了报UNABLE_TO_VERIFY_LEAF_SIGNATURE。servername这个字段容易被省略,但在用 IP 直连、或者目标和 hostname 不一致的场景下,它决定了 SNI 发什么、证书按哪个名字校验,省掉它经常是莫名其妙失败的原因。
如果不想改代码,用环境变量更省事:
# Node 全局追加根 CA,注意是追加不是替换 export NODE_EXTRA_CA_CERTS=/opt/certs/rootCA.crt # curl 用 --cacert 指定,-v 打握手细节,--resolve 绕过 DNS curl -v --cacert /opt/certs/rootCA.crt \ --resolve dev.internal:443:192.168.1.20 \ https://dev.internal/healthNODE_EXTRA_CA_CERTS这个名字里带 EXTRA,说明它是追加到默认信任列表后面的,不会覆盖内置的公共 CA,这点比 Java 那个参数友好。curl的--resolve在本地测试时特别有用,可以直接把域名指向某个 IP,不用改 hosts 文件,测完就失效。
-v的输出里重点看三段:Server certificate里 CN 和 SAN 对不对、SSL certificate verify ok有没有出现、subjectAltName是否包含你请求的地址。这三段能对上,说明证书和服务端配置都没问题了。
5. 避坑与排查:五个把本地 HTTPS 卡住的真实故障
这一章的每条都是我在实际环境里反复遇到的,按「现象 → 原因 → 解决」的顺序写,照着对号入座基本能定位。
5.1 浏览器仍报红叉:SAN 里少了 IP 或者用了通配符
现象:证书导入到系统信任库了,浏览器还是报NET::ERR_CERT_COMMON_NAME_INVALID,点开详情看证书主体,CN 明明写的就是访问的域名。
原因:现代浏览器早已不读 CN 字段,只校验 SAN。CN 里写对了,SAN 里没写,一样不通过。另一种情况是用了*.dev.internal通配符,但访问的是a.b.dev.internal——通配符只匹配一级,多级子域覆盖不到。
解决:重新签发,把访问用到的每个域名和 IP 都塞进 SAN。通配符只在「同级子域数量多且不固定」时才划算,否则老老实实列全。签完后用openssl x509 -noout -ext subjectAltName确认一遍,别只看向导界面。
5.2 Java 客户端报 PKIX path building failed
现象:浏览器和 curl 都能正常访问,只有 Java 服务调不通,异常栈里是sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target。
原因:Java 用的是自己的信任库,和操作系统的证书store完全独立。你把根 CA 装进了 Windows 证书管理器,对 Java 一点用没有。还有一种隐蔽情况:应用启动参数里指定了自定义 truststore,而你导入的是默认的cacerts。
解决:先确认实际生效的 truststore 路径,再往里导。用keytool -list -keystore xxx.jks看别名在不在。如果证书链是多层的,要把整条链都导进去,只导根证书在部分场景下仍然会失败——这种情况用-rfc参数导 PEM 格式,或者干脆导 p7b。
5.3 Nginx 起不来或者握手直接断
现象:nginx -t报SSL_CTX_use_PrivateKey("/etc/nginx/certs/server.key") failed,或者服务能起来但客户端一连就断,日志里是SSL_do_handshake() failed。
原因:前者基本可以确定是证书和私钥不配对,或者私钥文件被加密了 Nginx 读不了。后者多半是证书链顺序反了——ssl_certificate指向的文件里,中间证书跑到了站点证书前面。
解决:配对问题用第 3 章那两条openssl sha256命令比对公钥哈希。链顺序问题用openssl s_client -connect dev.internal:443 -showcerts看服务端实际发出的顺序,第一个必须是站点证书。顺带检查私钥文件权限,Nginx 的 worker 进程用户必须可读。
5.4 证书「还没生效」或者装完就过期
现象:客户端报certificate is not yet valid,或者明明刚生成的证书,剩余有效期只剩几天。
原因:notBefore和notAfter是按生成机器的系统时间写的。如果生成证书的那台机器时钟漂移了,签出来的证书时间就是错的。另一种情况是模板里把有效期单位搞错了,误把「天」填成了「月」或者反过来。
解决:生成前先对时,容器环境尤其注意,容器启动时的时间可能和宿主机差很多。签完之后用openssl x509 -noout -dates看一眼起止时间是否符合预期。有效期这种参数一旦签出去就改不了,只能重签,所以养成生成后立刻检查的习惯。
5.5 换台机器就连不上,把私钥当证书到处拷
现象:本地调试一切正常,部署到测试环境就握手失败,报no shared cipher或者直接连接被拒。
原因:拷贝的时候把.key当成.crt发出去了,或者反过来。这两个文件在某些导出方式下名字很像,尤其在打包压缩后只看文件名分辨不出来。还有一种是把根 CA 的私钥拷到了应用服务器上——这属于严重的安全问题,不报错但风险极高。
解决:建立固定的目录约定,比如ca/放根证书和根私钥(不进代码仓库),server/放站点证书和站点私钥,dist/放对外分发的公开证书。私钥文件的权限设成600,并且明确一条规矩:根 CA 私钥永远不出现在任何应用服务器上。传输前用openssl rsa -in xxx.key -check确认文件类型,别靠猜。
6. 再进一步:mTLS 双向认证与到期轮换脚本化的做法
单向认证只验服务端,双向认证(mTLS)两头都验,服务端会要求客户端出示证书。配置本身不复杂,难的是把「签发客户端证书」纳入同一套流程。
服务端侧在 Nginx 里开三个开关:ssl_verify_client on表示强制校验,ssl_client_certificate指向签发客户端证书的那个 CA 的根证书,ssl_verify_depth控制链的深度。客户端证书的ExtendedKeyUsage必须是clientAuth,签成serverAuth的话握手会被拒,报unsupported certificate purpose,这个报错看不出是用途问题,很容易卡住。
比配置更容易出问题的是轮换。证书到期这件事不会提前通知你,所以我一般用一个检查脚本挂在定时任务里:
#!/usr/bin/env bash # check-cert-expiry.sh:扫描目录下所有 pem/crt,剩余天数低于阈值就返回非零 set -euo pipefail CERT_DIR="${1:-/etc/nginx/certs}" THRESHOLD="${2:-30}" fail=0 for f in "$CERT_DIR"/*.pem "$CERT_DIR"/*.crt; do [ -e "$f" ] || continue # -checkend 直接判断剩余秒数,比手工算日期可靠 if openssl x509 -in "$f" -noout -checkend $((THRESHOLD * 86400)) > /dev/null 2>&1; then echo "OK $f" else echo "WARN $f 将在 ${THRESHOLD} 天内过期" fail=1 fi done exit $fail这个脚本的要点在于用-checkend而不是自己解析日期字符串,少一层出错的可能。set -euo pipefail那行是防止某个文件读取失败后脚本静默跳过,导致「全部 OK」的假象。退出码非零可以接告警,接不上告警至少也能在 CI 里拦住一次带过期证书的发布。
轮换的动作建议写成固定流程:重新生成站点私钥和 CSR、用根 CA 重签、重新拼接 fullchain、nginx -t验证、reload。顺序别颠倒,先验证再 reload,reload 失败还能回滚,直接 restart 失败就是服务中断。至于客户端信任库,根 CA 不变的话不用动;如果连根 CA 一起换,那就得走一次全量分发,这种情况我一般会保留新旧两个根同时在信任库里跑一个过渡期,避免了「切换瞬间全部掉线」。
我第一次做 mTLS 上线时,卡了整整半天,最后发现是客户端证书的用途写成了serverAuth,而报错信息里只有一句handshake failure,完全没有指向性。从那以后我每次签完证书,不管多急,都强制走一遍openssl x509 -text看扩展段,SAN、KeyUsage、ExtendedKeyUsage 三项逐条对一遍再往下走。这套流程看着笨,但省下来的排查时间远超那几十秒。希望帮到你。
本文还有配套的精品资源,点击获取