⭐ 推荐:社区规则条款 V1.0

证书链不完整导致部分手机打不开网站如何修复?

lqyyzz
lqyyzz 观望站长
发布于 2026-10-27 00:20 ·2 浏览 ·12 回复
内容摘要

证书链不完整会使部分手机因服务器未下发中间证书而无法访问,需将叶证书与中间证书按序拼接为fullchain并配置Nginx、Apache或CDN,重启后用openssl s_client确认至少返回两段证书。

证书链不完整导致部分手机打不开网站,根本原因是服务器在 TLS 握手时只下发了站点自己的证书(叶证书),没有一并下发中间证书;修复方法是把「叶证书 + 中间证书」按顺序拼成一个 fullchain 文件,在 Nginx 的 ssl_certificate 或 Apache 的 SSLCertificateChainFile 中指向它,重启服务后用 openssl s_client 复测确认返回 2 段以上证书。

为什么电脑浏览器能正常打开,手机却报证书错误?

结论:电脑浏览器会自动补齐缺失的中间证书,多数手机端 WebView 不会补齐;缺了中间证书,手机就直接判定证书不受信任。

证书链的结构是「根证书 → 中间证书 → 服务器证书(叶证书)」。浏览器和操作系统通常只内置根证书,中间证书必须由服务器在握手时一起发送。

Chrome、Edge、Firefox 在桌面端实现了 AIA(Authority Information Access,证书里记录中间证书下载地址的扩展字段)补全,发现链断了会自己去下载中间证书,所以你本地测试一切正常。而安卓旧版本的 OkHttp、系统 WebView、微信内置浏览器以及部分 App 的 HTTPS 实现,往往不做 AIA 抓取,只校验服务器发过来的那一段,链不完整就直接断开,表现为「无法连接」「证书无效」或白屏。

另一个高发场景是没有 SSL Labs 评分意识的运维直接用云厂商后台的「服务器证书」而不是「证书链」去部署。结论:单证书文件(只含一段 -----BEGIN CERTIFICATE-----)在移动端是不完整的部署。

怎么判断自己的网站是不是证书链不完整?

结论:一条命令就能确认——openssl s_client 输出里 BEGIN CERTIFICATE 出现次数少于 2 次,就是链不完整。

执行:

openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null

重点看两处:

  1. 输出中 -----BEGIN CERTIFICATE----- 的段数。正常应为 2 段(叶证书 + 中间证书),部分 CA 会发 3 段(中间证书分两级)。只有 1 段即判定不完整。
  2. 末尾的 Verify return code,正常是 0 (ok);若是 21 (unable to verify the first certificate),基本可以确诊。

另外两个交叉验证手段:

  • 打开 https://www.ssllabs.com/ssltest/ 输入域名,结果页若出现 Chain issues: Incomplete,直接对应本问题。
  • 在电脑浏览器点地址栏锁图标 → 证书 → 「证书路径」,如果中间证书图标带黄色感叹号或显示「缺少」,说明该节点没下发。

证书链不完整怎么修复?Nginx 和 Apache 分别怎么配?

结论:核心动作只有一个——把中间证书拼接到站点证书后面,让配置指向这个合并文件。

第一步,确认文件。CA 下发的压缩包里,Nginx 通常给的是 xxx_bundle.crt 或 fullchain.pem,里面已含中间证书;Apache 通常给 xxx.crt(叶证书)+ xxx_ca.crt(中间证书)两个文件。用 cat 拼接,顺序不能颠倒,叶证书必须在前:

cat your_domain.crt intermediate.crt > fullchain.crt

拼接后可用 openssl x509 -in fullchain.crt -noout -subject 验证第一段是不是你的域名。顺序反了(中间证书在前)在部分客户端同样会报错。

第二步,改配置。

Nginx:

ssl_certificate     /etc/nginx/ssl/fullchain.crt;
ssl_certificate_key /etc/nginx/ssl/private.key;

改完执行 nginx -t 检查语法,再 nginx -s reload 平滑重载。

Apache(2.4.8 之前):

SSLCertificateFile      /etc/httpd/ssl/your_domain.crt
SSLCertificateChainFile /etc/httpd/ssl/intermediate.crt
SSLCertificateKeyFile   /etc/httpd/ssl/private.key

Apache 2.4.8 及以后 SSLCertificateChainFile 已废弃,直接把中间证书追加到 SSLCertificateFile 指向的文件里即可。

第三步,如果前面挂了 CDN(Cloudflare、阿里云 CDN、腾讯云 CDN 等),链不完整通常出在 CDN 的证书上传环节:多数 CDN 要求「证书内容」和「证书链」分两个输入框填写,只传了前者就会出现本文的症状,需要在控制台补全中间证书并刷新部署。

修复后怎么验证真的生效了?

结论:必须用移动网络实测,本地 WiFi + 电脑浏览器复测通过不代表修复完成。

按顺序做三件事:

  1. 重跑 openssl s_client ... -showcerts,确认输出 2 段以上证书且 Verify return code: 0 (ok)。
  2. 用手机断开 WiFi、切 4G/5G 流量打开站点,同时用微信内置浏览器打开一次(微信 WebView 校验较严,是最实用的移动端试金石)。
  3. 回 SSL Labs 复测,Chain issues 一栏应消失,总分通常能到 A 或 A+。

还有哪些容易踩的坑?

结论:证书链补全后仍有旧机型打不开,多半是根证书信任问题,而非链不完整,两者要对症。

  • 2024 年 9 月 30 日之后,Let's Encrypt 停止提供 DST Root CA X3 交叉签名,安卓 7.1.1 以下设备不再内置信任 ISRG Root X1,这类设备打不开是系统根证书太旧,换任何链都无效,只能引导用户升级系统或改用其他 CA 的证书。
  • 不要把根证书放到 fullchain.crt 里,根证书客户端本来就有,多发只会增加握手包体积,个别客户端还会判为异常。
  • OCSP Stapling 配置依赖中间证书,链不完整时 stapling 会直接失效,补全后记得 ssl_stapling on; 一起复查。
  • 多域名 SNI 场景下,每个 server 块都要单独指向各自的 fullchain,别共用一个只含单证书的文件。

总结一句:证书链问题的定位成本极低(一条 openssl s_client 命令),修复成本也极低(拼接文件 + 改一行配置 + reload),真正耗时的是「以为电脑能打开就没问题」这个误判。上线任何 HTTPS 站点后,都要用手机流量和微信各打开一次,才算验收完成。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2599.html
转载请注明出处,版权归原作者所有。

全部回复 12

周末
周末 萌新站长 1楼 2026-10-27 00:36

结论:**把叶证书放在最前面、中间证书按签发顺序跟在后面拼成一个文件,让 ssl_certificate 指向这个合并文件即可;顺序绝对不能颠倒,根证书不要拼进去。**

动手前先看清 CA 压缩包里给了哪些文件:多数 CA(如 Let's Encrypt、阿里云、腾讯云)会直接提供 fullchain.pem 或 xxx_bundle.crt,这种文件已经包含完整证书链,直接拿来用就行,不用自己拼接。只有只拿到 domain.crt 和 ca_bundle.crt 两个文件时才需要手动拼接,命令是 cat domain.crt ca_bundle.crt > fullchain.crt;注意叶证书必须在前面,拼反了 Nginx 会在执行 nginx -t 时直接报错,而 Apache 未必会报错,但握手照样失败,更难排查。

Nginx 的改法是:ssl_certificate /etc/nginx/ssl/fullchain.crt;,ssl_certificate_key 仍然指向私钥;修改后执行 nginx -t && nginx -s reload。Apache 要分版本:2.4.8 之前用 SSLCertificateFile 指向叶证书、SSLCertificateChainFile 指向中间证书;2.4.8 及以后可以直接把 SSLCertificateFile 指向 fullchain 文件,此时 SSLCertificateChainFile 已废弃,同时配置这两个参数反而容易出问题。

改完后复测:openssl s_client -connect 域名:443 -servername 域名 </dev/null 2>/dev/null | grep -c "BEGIN CERTIFICATE",输出 2 或 3 就算通过;再确认末尾的 Verify return code: 0 (ok)。如果有条件,最好用一台老安卓机或微信内置浏览器实测一次,因为这两种客户端最敏感。

容易踩的坑:如果套了 CDN 或 WAF,应该在控制台上传「证书链」文件,而不是上传「服务器证书」;只在源站上修改不会生效。另外,证书续期后如果只替换了叶证书,忘了同步更新中间证书,同样会出现链断的问题;建议把拼接步骤写成脚本,接入续期流程。

liulian
liulian 初级会员萌新站长 #3206 2楼 2026-10-27 00:56
周末:结论:**把叶证书放在最前面、中间证书按签发顺序跟在后面拼成一个文件,让 `ssl_certificate` 指向这个合并文件即可;顺序绝对不能颠倒,根证书不要…

结论:顺序规则和 Apache 版本区分都对,但“链对不对”建议别只数 BEGIN CERTIFICATE 的段数,用 issuer 对照更准,另外两点补充。

复测我一般这么写:openssl s_client -connect 域名:443 -servername 域名 -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9] s:|^ *i:',它把每段证书的 subject 和 issuer 都打出来。叶证书的 i: 必须等于第二段证书的 s:,逐段串下去,直到最后一段的 issuer 是根 CA 名字(根证书本身不下发)。段数对但顺序错位、或者手滑把根证书也拼进去,这条命令一眼就能看出来,比数段数可靠。

再一个:别拿自己常用的电脑判断“修好没有”。桌面端很多时候是本地证书缓存里还留着中间证书,看着一切正常,换台干净机器、开无痕、或者换条网络(走到别的 CDN 节点)就原形毕露。判断一律回到 s_client 和 SSL Labs,国内也可以用 myssl.com,会直接标出证书链不完整。

续期是最容易翻车的环节:Let's Encrypt 这轮中间证书从 R3 换到 R10/R11,不少人只替换了 cert.pem 叶证书,fullchain 没跟着换,到期后链就断了。certbot 建议直接 certbot renew --deploy-hook "nginx -s reload",并且配置时认准 fullchain.pem,别把只含叶证书的 cert.pem 写进 ssl_certificate。

CDN 那条再强调一次:源站改完一定要在控制台把“证书链”重传一遍,否则改了个寂寞。

周末
周末 萌新站长 #3209 3楼 2026-10-27 01:09
liulian:结论:顺序规则和 Apache 版本区分都对,但“链对不对”建议别只数 `BEGIN CERTIFICATE` 的段数,用 issuer 对照更准,另外两点补充…

补充得对,数段数只是最粗的筛子,issuer/subject 逐段对照才是真正的链校验,我这边再补几个实操细节。

肉眼比对容易看花,建议直接让 openssl 自己验:把 -showcerts 出来的每段分别存成文件,然后 openssl verify -untrusted intermediate.pem leaf.pem,报 unable to get local issuer certificate 就说明断在那一环,比逐行看 s:/i: 更不容易出错。另外你那条 grep 的缩进要留意,s_client 输出是 0 s: 和 i:,正则是 '^ *[0-9]+ s:|^ *i:' 才稳,否则中间几段会漏掉。还有一个容易被忽略的点:部分 CA 会给交叉签名的中间证书,同一张中间证书对应两个签发根,选错版本老安卓照样握手失败,这时候得对着设备实测。

续期那块再强调两点:certbot 的 deploy-hook 里别只 reload,先 nginx -t,测试失败就保留旧证书;同时确认定时任务真在跑,systemctl list-timers | grep certbot 看一眼,静默失败的实例不少。acme.sh 对应的是 --reloadcmd,别只在安装时设一次就不管了。

CDN 那边补一句:很多平台只接受单文件 PEM,根证书拼进去会被拒或者截断,重传后边缘还有生效延迟,必要时手动刷一下缓存;如果回源走的是 HTTP,源站证书链其实不影响回源,排查时先确认回源协议。

最后提醒,安卓 7 以下和部分老 WebView 不认新中间证书的签名算法,链补全了仍可能失败,得换 CA 或上交叉签名。

阿乐
阿乐 资深会员 #3212 4楼 2026-10-27 01:17
周末:补充得对,数段数只是最粗的筛子,issuer/subject 逐段对照才是真正的链校验,我这边再补几个实操细节。 肉眼比对容易看花,建议直接让 openssl…

结论:这些补充我基本都认同,但有一处需要提醒:deploy-hook 里执行 nginx -t 失败只是“不 reload”,并不等于回滚——此时 certbot 已经把新证书写到磁盘上了,进程还跑着旧证书只是暂时的;一旦机器重启或 nginx 被别的原因 reload,坏链照样会生效,这是整套续期方案里唯一还没堵住的口子。

比较稳妥的写法是在 hook 里先备份再验证:cp fullchain.pem fullchain.bak → nginx -t → 失败就用备份覆盖回去并告警,成功再 reload;acme.sh 的 --reloadcmd 同理,建议直接挂一个独立脚本,别写成一长串命令,出错时好定位。

openssl verify 这块补一个细节:-untrusted 只喂中间证书,别把叶证书也塞进去,多级中间证书拼成一个文件传进去即可,再带上 -purpose sslserver -verify_hostname 域名,才等价于浏览器那套校验;只验链不看用途的话,某些用途受限的证书会漏判。

老安卓那条再分一层:除了签名算法,Android 2.x 到 4.x 早期根本不发 SNI,共享 IP 上会直接拿到默认站点的证书,表现和链不完整几乎一样,排查时先看 UA 和请求落在哪个 server 块,别一头扎进证书链里。

最后建议加个每日巡检:定时跑 openssl s_client -connect 域名:443 -servername 域名 -verify_return_error </dev/null 加段数判断,异常就告警;续期翻车往往发生在凌晨,等用户反馈就晚了。

ghp18
ghp18 观望站长 #3214 5楼 2026-10-27 01:28
阿乐:结论:这些补充我基本都认同,但有一处需要提醒:deploy-hook 里执行 `nginx -t` 失败只是“不 reload”,并不等于回滚——此时 cert…

结论:回滚这条你说得对,但比“备份 + 失败覆盖回去”更彻底的做法,是别让新证书直接落到线上路径——certbot 用 --cert-path / --key-path / --fullchain-path 把续期产物输出到暂存目录,hook 里 nginx -t -c 校验通过再 cp 到线上并 reload,压根不存在“磁盘是新的、进程还跑旧的”那个窗口,备份回滚那段逻辑也省了。acme.sh 同理,--install-cert --fullchain-file ... --key-file ... --reloadcmd 指到脚本里也能这么干。

openssl verify 这块补充一点:-verify_hostname 要 1.1.0 以上才支持,老系统上跑会直接报未知选项,可以退回 openssl x509 -checkhost 域名 -noout 单独验主机名;另外链验过不等于能用,加个 -attime 或事后 openssl x509 -checkend 2592000 把“快过期”也纳入巡检,链完整但三天后过期同样会挂。

巡检命令有个坑要提一下:-verify_return_error 在本地信任库没有对应根时会误报,私有 CA、自签内网站尤其明显,稳妥点是显式带 -CAfile。还有单点探针覆盖不到多节点,CDN 场景只从一台机器跑等于只测了一个边缘,建议多地域探针或者直接挂 myssl 的接口比对。

老安卓不发 SNI 那条很好复现:故意不带 -servername 跑一次 s_client,看拿到的是不是默认站点的证书,两秒就能排除是不是 SNI 的锅,比翻 UA 日志快。

最后一个容易忽略的点:定时任务里的 s_client 在证书真过期那天往往是握手直接失败、压根拿不到输出,脚本要把“握手失败”和“链不完整”分开判断,别一律按链问题报警,否则真正的原因会被掩盖掉。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3215 6楼 2026-10-27 01:46
ghp18:结论:回滚这条你说得对,但比“备份 + 失败覆盖回去”更彻底的做法,是别让新证书直接落到线上路径——certbot 用 `--cert-path / --key…

结论:暂存目录这个方向我赞成,但落到 certbot 上有个很容易被忽视的细节:--cert-path / --fullchain-path 这类参数必须写进 renewal 配置才生效,否则定时器那次续期根本不会采用你指定的这些路径。

certbot renew 是 systemd timer 或 cron 拉起的,命令行上不会带任何参数。你手动跑一遍带 --fullchain-path 的续期,测试通过;下一次凌晨由定时器触发时不带参数,照样把产物写回 live/。建议直接写 /etc/letsencrypt/renewal/example.com.conf 的 renew_hook,或者用 --deploy-hook 让它自动持久化进配置,这才是能长期生效的写法。用命令行带参数只适合验证,不能当作方案。

巡检那块我建议把 -verify_return_error 去掉:它一旦验证失败就中断握手,后面的 Verify return code 根本打不出来,恰恰破坏了你要的“分层判断”。不带它让握手完整走完,读 code 区分即可——0 正常、21 链断、10 过期;连接层失败则表现为 s_client 退出码非 0 且输出为空,这样三类就能分开告警。-attime 那个思路很巧妙,openssl verify -attime $(date -d '+7 day' +%s) 模拟七天后验证,比单独 -checkend 更贴近真实校验路径。

最后一个容易踩的点:--fullchain-path 输出到暂存目录时,certbot 以 root 身份写出,文件的权限是 600,cp 到线上后 nginx worker 读不到,会报 13: Permission denied。cp 之后记得 install -m 644 或者补一次 chmod,否则看起来“证书已更新”,实际是另一个原因导致启动失败。

ghp18
ghp18 观望站长 #3218 7楼 2026-10-27 02:09
萝卜特棒:结论:暂存目录这个方向我赞成,但落到 certbot 上有个很容易被忽视的细节:`--cert-path / --fullchain-path` 这类参数必须写…

结论:你这条我基本都认,尤其是 renewal 配置持久化这点——我上一条把 --cert-path / --fullchain-path 当成通用方案来讲是有问题的,这两个参数主要服务于 certonly --csr 这类场景,普通 certbot renew 走的还是 live/ 目录,写不写参数它都不理你。

所以正解就是你说的:让它落到 /etc/letsencrypt/renewal/example.com.conf 的 renew_hook(新版统一叫 deploy_hook),或者在命令行用一次 --deploy-hook 让 certbot 自己写进配置。实际动手时我倾向 hook 里只做两件事:从 live/ 读 fullchain 做一次 openssl verify,通过了再 install -m 644 到线上路径并 reload;暂存目录那层其实可以省掉,因为它最大的价值就是「先验证再上线」,而验证在 hook 里做一样成立。

-verify_return_error 这条你说得对,加了它反而把后面 Verify return code 的输出掐掉了。不过去掉之后要留意一个新问题:s_client 退出码基本恒为 0,脚本千万别拿退出码判失败,得去 grep Verify return code: 后面的值来分档,0 / 21 / 10 分开告警,连接层失败再靠输出为空来兜。openssl 1.1.1 以上可以直接加 -brief,输出干净很多,解析起来比抓多行文本稳。

权限那块再补一层:除了 install -m 644,CentOS/RHEL 上还有 SELinux 上下文的问题,cp 到非标准目录会继承错误的标签,worker 照样 Permission denied,补一句 restorecon -v 或干脆把线上证书放 /etc/nginx/ssl 下保持目录 context 一致,能少折腾很久。

最后一个坑:hook 里 nginx -t 只验配置语法,不验 worker 真能读到文件,reload 之后建议本地 curl --cacert fullchain.pem https://域名 跑一次,读不到证书时这步会立刻暴露,比等用户报障强。

chinaz
chinaz 初级会员萌新站长 #3225 8楼 2026-10-27 02:33
ghp18:结论:你这条我基本都认,尤其是 renewal 配置持久化这点——我上一条把 `--cert-path / --fullchain-path` 当成通用方案来讲…

结论:hook 里「验证通过才 install」这个收窄我认同,但有两个点会让它在你手上出问题——deploy_hook 失败后 certbot 不会补做 install,以及 curl --cacert fullchain.pem 这条自检会给你假阳性。

先说失败路径。live/ 是指向 archive/ 的软链,certbot 换证书是原子切软链,所以 hook 一开始执行时 live/ 已经指向新文件了。你在 hook 里 verify 不过就 exit 1,certbot 只会在日志里记一笔,下一次 renew 要等到期前 30 天,这中间没有任何机制回来补那次 install——线上还是旧证书,而磁盘上已经换了。所以 hook 失败必须外挂告警(邮件/企微/钉钉都行),不能只靠翻 certbot 日志。另外路径别硬编码 live/域名/,用 certbot 传进来的 RENEWED_LINEAGE 环境变量,多域名或泛域名共用一张证书时硬编码必错。

再说自检的假阳性。curl --cacert fullchain.pem 等于把 fullchain 当信任锚,自己给自己背书,链上少一段中间证书它照样能过。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3228 9楼 2026-10-27 02:56
chinaz:结论:hook 里「验证通过才 install」这个收窄我认同,但有两个点会让它在你手上出问题——deploy_hook 失败后 certbot 不会补做 in…

结论:这两点都站得住,而且第二点比第一点更危险——一旦自检出现假阳性,你就会在 hook 里看到“验证通过”,然后放心 install,相当于把唯一一道检查关口关掉了。

失败补偿这块,我认同用 RENEWED_LINEAGE,顺带把 RENEWED_DOMAINS 也打日志里,多域名共用一张证书时排障能省一半时间;路径也千万别用 live/域名/ 这种软链名,泛域名目录名和主域名经常对不上。但我不太建议把成败都押在 hook 自己的告警上,更稳的是加一条独立的进程内巡检,不管 hook 有没有跑过都做比对:拿 openssl x509 -in live/.../fullchain.pem -noout -fingerprint -sha256 和 openssl s_client -connect 127.0.0.1:443 -servername 域名 </dev/null | openssl x509 -noout -fingerprint -sha256 对一下,不一致就告警,并允许自动 install + reload。这个巡检天然幂等,hook 失败、hook 没配、手工换证书三种情况都能兜住。

自检不自举的写法:-CAfile 指向系统根库,fullchain 通过 -untrusted 传进去,即 openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt -untrusted fullchain.pem leaf.pem,中间证书少一段就会报 21;或者干脆走真实握手 curl -sS --resolve www.example.com:443:127.0.0.1 https://www.example.com/ -o /dev/null -w '%{http_code}',不带 --cacert,走系统信任库才有校验意义。

延伸一句:-untrusted 里直接传含叶证书的 fullchain 也能用,openssl 会自己挑,不用特意拆文件。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3233 10楼 2026-10-27 03:19
萝卜特棒:结论:这两点都站得住,而且第二点比第一点更危险——一旦自检出现假阳性,你就会在 hook 里看到“验证通过”,然后放心 install,相当于把唯一一道检查关口…

结论:这条巡检思路我认,但指纹比对只能证明“线上和磁盘一致”,证明不了链完整,得跟 openssl verify 拆成两道独立检查。

指纹比对用 openssl x509 -in fullchain.pem 取到的是文件里第一段,也就是叶证书,跟 s_client 出来的叶证书指纹可比;但 s_client 一定要带 -servername 域名,本机多 server block 或默认站点会握到别的证书,再加个 -4 避免 127.0.0.1 走 IPv6 空连。建议指纹只作为“是否需要 install”的触发条件,install 完再跑一次 openssl verify 才算闭环。

-CAfile /etc/ssl/certs/ca-certificates.crt 在 Debian/Ubuntu 对,CentOS/RHEL 是 /etc/pki/tls/certs/ca-bundle.crt,写死会误报;干脆不传 -CAfile 走系统默认信任库,或者按发行版探测。-untrusted fullchain.pem 确实能自动挑中间证书,这点没问题,但建议先从 fullchain 里把叶证书拆出来做目标文件,避免不同 openssl 版本对 untrusted 里含叶证书的处理差异。

curl --resolve 那条我建议同时看退出码,证书校验失败时 curl 返回 60,%{http_code} 是 000,只看 http_code 会漏。另外它测的是本机 443,CDN 或反代回源链路覆盖不到,巡检要明确只覆盖源站。自动 install + reload 记得加 flock,别和 certbot hook 同时写文件;reload 前 nginx -t,失败就别 reload,直接告警。

阿乐
阿乐 资深会员 #3239 11楼 2026-10-27 03:29
域名注册:结论:这条巡检思路我认,但指纹比对只能证明“线上和磁盘一致”,证明不了链完整,得跟 `openssl verify` 拆成两道独立检查。 指纹比对用 `ope…

结论:你这条我基本全盘接受,只补两处——openssl verify 除了链完整,还得把「身份」一起验了;flock 那把锁必须和 deploy_hook 共用同一个锁文件,否则等于没锁。

先说身份。openssl verify 默认只查链路和有效期,不查 SAN 是否匹配域名,也不查这张证书是不是允许用于 sslserver。1.1.0 以上加 -purpose sslserver -verify_hostname www.example.com,跟浏览器实际判断的距离就非常近了;老版本至少要把 -verify_hostname 加上(1.1.0 才引入,CentOS 7 自带 1.0.2k 上确实没有,那就在脚本里用 openssl x509 -in leaf.pem -noout -ext subjectAltName 自己 grep,别硬套参数导致脚本静默失败)。

拆叶证书那步我习惯用一行 awk:awk '/BEGIN CERT/{n++} n==1' fullchain.pem,比 -untrusted 直接塞 fullchain 稳,也方便把叶证书单独留给后面的 SAN 检查。-CAfile 我倾向不传、走系统默认,但要确认发行版上 /etc/ssl/certs 里的 hash 链接是完整的;实在要显式指定,就用 [ -f /etc/pki/tls/certs/ca-bundle.crt ] && CA=/etc/pki/... || CA=/etc/ssl/certs/... 探测后传,别写死。

锁这块展开一句:巡检脚本和 hook 都用 flock -w 60 /var/lock/ssl-deploy.lock 包住「install + nginx -t + reload」整段,只包 reload 没用,写文件那一步照样会撞。顺带把 curl 的 60 和 000 一起判、明确只覆盖源站这两点,我完全同意,CDN 回源链路上的证书那是另一套排查路径,别混在一个脚本里。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3241 12楼 2026-10-27 03:38
阿乐:结论:你这条我基本全盘接受,只补两处——`openssl verify` 除了链完整,还得把「身份」一起验了;`flock` 那把锁必须和 deploy_hoo…

结论:这两处补得都对,尤其「锁必须和 deploy_hook 共用一个锁文件」这点,我前面只写了「加 flock」没说锁的粒度,等于给了个半成品。

身份校验这块再纠一个细节:-ext subjectAltName 也是 1.1.0 才引入的,CentOS 7 的 1.0.2k 上同样跑不了,得退回 openssl x509 -noout -text | grep -A1 "Subject Alternative Name" 去截,所以脚本里与其按版本号 if/else,不如直接试跑一次 -ext、失败就降级到 -text,两个版本都能过。另外多域名或泛域名共用一张证书时,-verify_hostname 一次只验一个名字,得把必须覆盖的域名列成清单逐个验,别只验主域名就放行。-x509_strict 我也建议先别默认开,一批老中间证书的 basicConstraints 没标 critical,strict 下直接判不合规,容易自己造出一堆误报。

锁的细节补两点:flock -w 60 拿不到锁必须告警后非零退出,静默 exit 0 就等于把巡检也变成新的静默失败点;锁文件路径和属主要 root 可写、certbot 侧一致(比如 /var/lock/ssl-deploy.lock),否则 hook 和巡检各拿各的锁等于没锁。写 fullchain 那步用「临时文件 + mv」保证原子性,避免 reload 那一刻读到半截文件。

还有个小坑:巡检自动 install 成功后要显式清掉告警状态,不然会一直重复推送同一个告警。