SSL 证书错误怎么解决:常见报错原因与排查方法
SSL 证书错误按报错代码分四类:证书过期或电脑时间不对(ERR_CERT_DATE_INVALID)、证书不受信任(ERR_CERT_AUTHORITY_INVALID,多为自签名或缺中间证书)、域名不匹配(ERR_CERT_COMMON_NAME_INVALID)、TLS 握手失败。先用 openssl s_client 看清服务器返回的证书,再到 Nginx 里对症修改。
SSL 证书错误的排查顺序是:先看报错代码判断类型,再用命令确认服务器实际返回了什么证书,最后改服务器配置。浏览器提示“您的连接不是私密连接”“此网站的安全证书有问题”时,页面上那串 NET::ERR_CERT_… 代码就是线索;同样的问题在 curl、Java、Python 等程序里会以另一种措辞出现,下面这张表把它们对应起来。
先按报错代码判断原因
| Chrome / Edge | Firefox | curl、程序里的报错 | 含义 | 先查什么 |
|---|---|---|---|---|
| NET::ERR_CERT_DATE_INVALID | SEC_ERROR_EXPIRED_CERTIFICATE | certificate has expired | 证书过期或还没生效 | 证书到期日;访客电脑的系统时间 |
| NET::ERR_CERT_AUTHORITY_INVALID | SEC_ERROR_UNKNOWN_ISSUER、MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT | unable to get local issuer certificate、self-signed certificate、PKIX path building failed | 证书链无法追溯到受信任的根 | 是否自签名;是否缺中间证书;是否被代理或杀毒软件拦截 |
| NET::ERR_CERT_COMMON_NAME_INVALID | SSL_ERROR_BAD_CERT_DOMAIN | no alternative certificate subject name matches | 证书里没有访问的这个域名 | 证书的 SAN 列表;是否返回了别的站点的证书 |
| NET::ERR_CERT_REVOKED | SEC_ERROR_REVOKED_CERTIFICATE | 使用 OpenSSL 的 curl 默认不检查吊销,往往不报错 | 证书已被吊销 | 是否申请过吊销、重签后还在用旧证书 |
| ERR_SSL_VERSION_OR_CIPHER_MISMATCH | SSL_ERROR_NO_CYPHER_OVERLAP、SSL_ERROR_UNSUPPORTED_VERSION | handshake failure、alert protocol version | 双方没有共同支持的协议版本或加密套件 | 服务器是否只开了 TLS 1.0/1.1;套件是否过旧 |
| ERR_SSL_PROTOCOL_ERROR | SSL_ERROR_RX_RECORD_TOO_LONG | wrong version number | 常见原因是 443 端口返回的不是 TLS | 是否把 HTTP 服务开在了 443;端口转发是否指错 |
表里前三类最常见。Python 的 requests 库遇到证书校验失败,统一报 CERTIFICATE_VERIFY_FAILED,后面跟着的具体原因同样能套进这张表。
用 openssl s_client 三条命令定位
浏览器的提示只告诉你“不行”,openssl s_client 能告诉你服务器到底发了什么。以下命令在任何一台装有 OpenSSL 的 Linux 或 macOS 上都能运行,-servername 最好每次都带上:OpenSSL 1.1.1 以前的版本不会自动发送 SNI,用 IP 连接时也不会发送,多站点共用一个 IP 时,你看到的就可能是另一个站点的证书。
# 1. 看校验结果
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null | grep "Verify return code"
# 2. 看服务器发来的证书链:每一级的使用者(s:)和签发者(i:)
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null 2>/dev/null | grep -E "^ *[0-9] s:|^ *i:"
# 3. 看证书本身:域名、有效期、签发者
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
第 1 条命令的返回码对照:
| Verify return code | 含义 | 对应的浏览器报错 |
|---|---|---|
| 0 (ok) | 证书链完整,校验通过 | 无 |
| 10 (certificate has expired) | 证书已过期 | ERR_CERT_DATE_INVALID |
| 18 (self-signed certificate) | 服务器证书是自签名的 | ERR_CERT_AUTHORITY_INVALID |
| 20 (unable to get local issuer certificate) | 找不到签发者:证书链不完整,或本机信任库里没有对应的根证书 | ERR_CERT_AUTHORITY_INVALID |
| 21 (unable to verify the first certificate) | 只发了服务器证书,没有中间证书 | ERR_CERT_AUTHORITY_INVALID |
第 2 条命令正常时至少有两级:0 s: 是你的域名,1 s: 是中间证书,并且 0 的 i: 与 1 的 s: 相同。只有 0 一级,就是证书链不完整。
返回码 0 只说明证书链没问题,不检查域名。要连域名一起校验,加上 -verify_hostname,不匹配时返回码是 62 (hostname mismatch):
openssl s_client -connect 203.0.113.10:443 -servername example.com -verify_hostname example.com </dev/null 2>/dev/null | grep "Verify return code"
证书过期:ERR_CERT_DATE_INVALID
先分清是服务器的问题还是访客电脑的问题。用上面第 3 条命令看 notAfter,日期已过就是证书真的过期了;日期没过而只有个别访客报错,让对方检查电脑或手机的系统时间,时间被调到几年前或几年后都会报这个错,开启“自动设置时间”即可。
证书过期但明明已经续期,常见原因有三个:
- 续期了,没重载。acme.sh、certbot 更新了证书文件,但 Nginx 进程里加载的还是旧证书。检查续期命令是否带了重载(acme.sh 的
--reloadcmd、certbot 的--deploy-hook),手动执行一次nginx -t && systemctl reload nginx。 - 续期了,Nginx 引用的是另一个文件。新证书写到了 A 路径,配置里写的是 B 路径。用
nginx -T 2>/dev/null | grep ssl_certificate列出 Nginx 实际加载的证书路径,再对每个文件执行openssl x509 -noout -enddate -in 路径核对。 - 还有别的入口没换。CDN、负载均衡、另一台备机上的证书是各自独立的,要逐个检查。
证书不受信任:ERR_CERT_AUTHORITY_INVALID 与证书链不完整
这个错误的意思是浏览器无法从你的证书一路追溯到它信任的根证书。按出现频率排:
| 情况 | 怎么认出来 | 处理 |
|---|---|---|
| 缺中间证书 | 返回码 21 或 20;-showcerts 只有一级;桌面浏览器正常,App、小程序、curl 报错 |
把服务器证书和中间证书合并成一个文件,见最后一节 |
| 自签名证书 | 返回码 18;签发者和使用者相同 | 面板或程序自动生成的测试证书,换成 CA 签发的正式证书 |
| 中间证书放错或过时 | 链里有两级,但 0 的 i: 和 1 的 s: 对不上 |
从签发机构重新下载与当前证书配套的中间证书 |
| 网络中有 HTTPS 拦截 | 只在某个网络或某台电脑出现,证书的签发者是杀毒软件、公司网关或上网行为管理设备的名字 | 不是服务器的问题;关闭杀毒软件的 HTTPS 扫描,或在公司网络中安装其根证书 |
| 老设备不信任新根证书 | 只有很老的手机或系统报错,例如 Android 7.1.1 以前的设备访问 Let’s Encrypt 证书 | 设备系统已无法更新时,只能换一家根证书更老的 CA,或请访客升级设备 |
缺中间证书最隐蔽,因为站长自己用电脑浏览器打开一切正常。部署完证书后,在 Linux 机器上用 curl -I https://www.example.com 测一次:Linux 上的 curl 不会自动补中间证书,它不报错才说明链是完整的。Windows 自带的 curl 走系统证书库,可能自动补全中间证书,测不出这个问题。
域名不匹配:ERR_CERT_COMMON_NAME_INVALID
浏览器只看证书的 SAN(使用者可选名称)列表,访问的域名不在列表里就报这个错。用第 3 条命令的 subjectAltName 输出对照,常见的几种情况:
- 只签了 www,没签根域名(或者反过来)。访客输入
example.com就报错。重新签发时两个都写上。 - 通配符只管一级。
*.example.com覆盖www.example.com、api.example.com,不覆盖example.com,也不覆盖a.b.example.com。 - 用 IP 访问。证书签的是域名,用
https://203.0.113.10访问自然对不上,这不是故障;确实要用 IP 访问的,得专门申请包含该 IP 的证书。 - 服务器返回了别的站点的证书。一台服务器上有多个 HTTPS 站点时,Nginx 会按 SNI 中的域名找对应的 server 块;某个域名没有配置 443 的 server 块,就会落到默认站点,返回默认站点的证书。用
-servername分别测各个域名,返回的证书不对,就补上该域名的 443 server 块,或者检查server_name是否写漏。 - 公共 Wi-Fi 的认证页面。连上酒店、机场 Wi-Fi 后还没登录,HTTPS 请求被劫持到认证页,于是打开什么网站都报证书错误,常见的就是域名不匹配。先在浏览器打开任意 http 页面完成认证。
握手失败:协议版本、加密套件与端口错配
握手失败时浏览器拿不到证书,报的是 ERR_SSL_… 而不是 ERR_CERT_…。主流浏览器已经不再支持 TLS 1.0 和 1.1,服务器只开了这两个版本,就会报 ERR_SSL_VERSION_OR_CIPHER_MISMATCH。分别测试服务器支持哪些版本:
openssl s_client -connect www.example.com:443 -servername www.example.com -tls1_2 </dev/null 2>&1 | grep -E "Protocol|Cipher is"
openssl s_client -connect www.example.com:443 -servername www.example.com -tls1_3 </dev/null 2>&1 | grep -E "Protocol|Cipher is"
能打印出协议版本和具体的套件名称,说明该版本可用;输出 Cipher is (NONE),或者出现 alert protocol version、handshake failure,说明不支持。Nginx 里把协议设成:
ssl_protocols TLSv1.2 TLSv1.3;
加密套件不要手写一长串过时的列表,用 Nginx 默认值,或参照 Mozilla 提供的 SSL 配置生成器里“Intermediate”档的配置。
ERR_SSL_PROTOCOL_ERROR 和 curl 的 wrong version number 则多半是 443 端口上跑的是普通 HTTP:Nginx 的 listen 443 漏写了 ssl,或者防火墙、端口映射把 443 转发到了后端的 80 端口。用 curl -v http://www.example.com:443/ 测一下,能返回 HTTP 响应就是这个原因。
在 Nginx 里修复:合并证书链、替换证书、重载
确认问题后,按下面的顺序修改,每一步都可以验证。
第一步,合并证书链。服务器证书在前,中间证书在后,顺序不能反:
cat example.com.crt intermediate.crt > /etc/nginx/ssl/example.com.fullchain.pem
grep -c '^-----BEGIN CERTIFICATE' /etc/nginx/ssl/example.com.fullchain.pem # 至少应为 2
数出来比证书张数少,多半是第一个文件末尾没有换行,两张证书粘在了同一行,改用 awk 1 example.com.crt intermediate.crt > 目标文件 合并即可。签发机构的下载包里,中间证书可能叫 chain.crt、ca-bundle.crt 或 _chain.crt;云服务商按 Nginx 格式打包的证书文件(文件名常带 bundle,或扩展名为 .pem)多数已经含有中间证书,先用上面的 grep 数一下再决定要不要合并。用 acme.sh、certbot 申请的,直接用它们生成的 fullchain 文件。
第二步,确认私钥和证书是一对。两条命令输出相同才配套:
openssl x509 -in /etc/nginx/ssl/example.com.fullchain.pem -noout -pubkey | sha256sum
openssl pkey -in /etc/nginx/ssl/example.com.key -pubout | sha256sum
第三步,修改配置并检查语法:
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example.com.fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
# 其余站点配置
}
nginx -t && systemctl reload nginx
nginx -t 的几种常见报错:
| 报错片段 | 原因 |
|---|---|
key values mismatch |
私钥和证书不配套;或者合并时把中间证书放在了前面,Nginx 拿第一张证书去和私钥配对 |
PEM_read_bio_X509_AUX() failed ... no start line |
找不到 PEM 开头行:文件可能是 DER 二进制、带了 BOM 头,或复制时在 -----BEGIN 前面多了空格 |
PEM_read_bio_PrivateKey() failed |
私钥文件格式不对,或者私钥设置了密码 |
cannot load certificate ... No such file |
路径写错;如果是权限问题,报的是 Permission denied |
第四步,从外部复测。回到第二节,三条命令再跑一遍:返回码为 0、证书链至少两级、到期日和域名都正确,问题才算解决。同一张证书还部署在 CDN、负载均衡或其他服务器上的,逐个入口测一遍。
常见问题
为什么电脑浏览器正常,手机 App 或小程序却报证书错误?
多半是服务器没有发送中间证书。桌面浏览器会自动下载或缓存缺失的中间证书,把问题掩盖了;很多 App、小程序、Linux 上的 curl 和编程语言的 HTTP 库不会这样做,于是校验失败。修复方法是在服务器上配置完整证书链,而不是在客户端里关闭校验。
能让访客点“继续访问”绕过证书错误吗?
普通的证书错误页可以手动选择继续,但网站下发过 HSTS 后,浏览器在有效期内不再提供继续的选项。绕过错误意味着无法确认对面是不是真正的服务器,登录、支付页面尤其不能这么做,应该尽快修好服务器端的证书。
网站接了 CDN,换了源站证书为什么浏览器还报错?
访客连接的是 CDN 节点,看到的是 CDN 上配置的证书,源站证书只影响节点回源这一段。证书错误出在访客侧时,要到 CDN 控制台更新或开启 HTTPS 证书;回源报错时才去改源站。