TLS 证书检查:openssl s_client 看有效期与链、到期前多久换、四种握手报错的原文
证书问题的特点是"浏览器能开、脚本连不上"或者反过来。openssl s_client 一次能给出四件事:协商到的协议版本、证书有效期、链是否验通、以及握手在哪一步失败。我通常先跑 -brief 确认通不通,再抓证书看日期和签发者,最后用 -verify_return_error 让验证失败真的变成失败——不加这个参数,s_client 会把验证错误打印出来然后继续握手。
四项检查与通过标准
| 检查项 | 命令 | 通过标准 | |
|---|---|---|---|
| 是否握手成功 | openssl s_client -connect host:443 -servername host -brief | 输出 CONNECTION ESTABLISHED 与 Protocol version | |
| 有效期 | `openssl s_client -connect host:443 -servername host </dev/null \ | openssl x509 -noout -dates` | notAfter 距今天数大于你的换证窗口 |
| 链是否可信 | openssl s_client -connect host:443 -servername host -verify 5 -verify_return_error | Verify return code: 0 (ok) | |
| 服务端发了几个证书 | `openssl s_client ... \ | grep -c "BEGIN CERTIFICATE"` | 至少含叶证书;中间证书缺失时链会断 |
| 到期倒计时 | openssl x509 -in chain.pem -noout -checkend 2592000 | 退出码 0 表示 30 天内不过期 |
实测一次公网域名(OpenSSL 3.0.22,容器内):
$ openssl s_client -connect example.com:443 -servername example.com -brief
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN = example.com
Verification: OK
$ openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -dates -subject -issuer
notBefore=Sep 26 22:49:11 2026 GMT
notAfter=Dec 25 22:56:35 2026 GMT
subject=CN = example.com
issuer=C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
notAfter 是 2026-12-25,检索日是 2026-11-10,剩 45 天。这个数字刚好落在"该安排了"的区间:公共 CA 的证书有效期普遍在 90 天上下,剩 45 天意味着已经过半。
到期前多久该换
| 剩余天数 | 动作 |
|---|---|
| 大于 30 天 | 正常,纳入例行巡检 |
| 30 天以内 | 安排换证,确认自动续期的任务真的在跑 |
| 14 天以内 | 视为事件处理,人工介入 |
| 已过期 | 先换证,再复盘为什么自动续期没生效 |
自动续期(ACME 一类)最常见的失效原因是续期任务本身没跑、或者域名验证失败,而不是证书逻辑有问题。所以"证书还有效"不能作为"续期链路正常"的证据,要单独验证一次续期任务能成功签发。
四种握手报错的原文
# 明文端口当 TLS 用
$ openssl s_client -connect example.com:80 -servername example.com
...:error:0A00010B:SSL routines:ssl3_get_record:wrong version number
# 端口不通
$ openssl s_client -connect 127.0.0.1:9
...:error:8000006F:system library:BIO_connect:Connection refused
# 自签证书 / CA 不在信任库
verify error:num=18:self-signed certificate
Verification error: self-signed certificate
Verify return code: 18 (self-signed certificate)
# 证书与主机名不匹配(需显式 -verify_hostname)
verify error:num=62:hostname mismatch
Verify return code: 62 (hostname mismatch)
四条报错对应四类原因,判断顺序是:先看是不是连到了明文端口(wrong version number 几乎一定是),再看端口是否可达(Connection refused 是网络层,不是证书问题),再看信任链(num=18 表示找不到可信签发者),最后看主机名(num=62)。
三个坑
第一,只看到期时间,不看链。服务器可能只发叶证书,中间证书缺失时,浏览器靠缓存或 AIA 抓取能补上,curl、Java、Python 这类客户端就直接失败。grep -c "BEGIN CERTIFICATE" 是判断"服务端到底发了几张"的最快方式,本次 example.com 只发 1 张,这在该站点是正常的(其签发者本身是受信根下的直签结构);但换成自建 CA 的场景,发 1 张往往就意味着中间证书漏配。
第二,把 -showcerts 的输出当成验证后的链。手册写得很明确:-showcerts 显示的是服务器发来的证书列表,按服务器发送顺序排列,它不是验证过的链。要判断链是否可信,看 Verify return code,或者用 -verify_return_error 让失败中止。
第三,s_client 是测试工具,默认不会因为验证失败而退出。手册明确说明它设计上会继续握手,因此"能连上"不代表"证书没问题"。两个配套动作:加 -verify_return_error 让错误变成失败;加 -verify_hostname <name> 校验主机名——不加的话,本次实测里 -servername 给一个不匹配的名字仍然返回 Verify return code: 0 (ok),因为它只验链不验名。
把四项检查写成一条巡检命令
H=example.com
openssl s_client -connect $H:443 -servername $H </dev/null 2>/dev/null > /tmp/chain.pem
openssl x509 -in /tmp/chain.pem -noout -dates -subject -issuer
openssl x509 -in /tmp/chain.pem -noout -checkend $((30*86400)) && echo "30 天内有效" || echo "30 天内到期"
grep -c "BEGIN CERTIFICATE" /tmp/chain.pem
四行输出对应四个判断:日期是否临近、主体是否是要检查的域名、checkend 的退出码、服务端发了几张证书。把它挂到每日巡检里,比等到用户报错再查要省事得多。注意 -checkend 的单位是秒,30*86400 就是 30 天;退出码 0 表示"在指定时间内不会过期",正好与直觉相反,脚本里别写反。
另一个容易忽略的点是证书链的顺序。服务端发来的列表应当从叶证书到中间证书、再到根的方向排列,顺序错了部分客户端会拒绝。-showcerts 的输出顺序就是服务端发送顺序,可以直接用来核对;这也是它虽然有局限但仍然有用的原因。
小结
一次完整的证书检查是四步:-brief 确认握手与协议版本,抓证书看 notAfter 算剩余天数,-verify_return_error 确认链可信,grep -c 确认服务端发了几张证书。四步都用同一组参数,写进巡检脚本后每次输出可直接对比。
报错归因按"明文端口 → 端口可达 → 信任链 → 主机名"的顺序走,能覆盖绝大多数握手失败。剩下的少数情况通常在协议或套件层(客户端太老、只支持 TLS 1.0、SNI 未发送),那时候再用 -tls1_2、-servername 这类参数做二分。
数据来源与检索时间
| 来源 | URL | 对应数据 |
|---|---|---|
| docs.openssl.org · openssl-s_client | https://docs.openssl.org/3.5/man1/openssl-s_client/ | -brief、-servername、-showcerts 只显示服务端发送的列表、-verify_return_error 让验证错误中止握手、"测试工具会继续握手"的说明、-verify_hostname |
| docs.openssl.org · openssl-x509 | https://docs.openssl.org/3.5/man1/openssl-x509/ | -checkend 的语义(到期返回非零)、-dates/-subject/-issuer、-ext subjectAltName |
检索时间:2026-11-10(Asia/Shanghai)。
核验说明
- 真跑:对
example.com:443的握手、证书日期/主体/签发者、-verify_return_error的Verify return code: 0 (ok)、证书张数(1 张)、-checkend 2592000(返回 0)与-checkend 2592000000(返回非零),均在debian:12-slim容器内实测。 - 真跑:
wrong version number(对 80 端口发 TLS)、Connection refused(连 127.0.0.1:9)、自签证书num=18、主机名不匹配num=62四类报错原文,均来自本机实际输出。自签与主机名两项用本地openssl req -x509生成证书并起s_server复现,主机名项需要-CAfile信任该证书后才暴露出num=62。 - 实测中的一个反例:对公网域名用
-servername wrong.example.org且不加-verify_hostname时,返回的仍是Verify return code: 0 (ok),说明 s_client 默认不校验主机名。该现象已写入正文,作为"要显式加-verify_hostname"的依据。 - 未验证:OCSP stapling、CRL 检查、TLS 1.0/1.1 的协商失败场景未构造。