ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

openssl显示证书有效期详解:本地查询、远程抓取与自动巡检

openssl显示证书有效期详解:本地查询、远程抓取与自动巡检

证书过期是我做运维这些年最不想遇到的故障之一:业务一切正常,线上却突然一批客户端握手失败,浏览器直接红色警告,抓包、查日志折腾半天,最后发现就是一张证书过了期。更麻烦的是,证书有效期信息不会自己跳到你眼前,得主动去查。而查这个事,绕不开 openssl。它既是命令行工具,也是排障利器,x509 和 s_client 这两个子命令组合一下,本地证书文件、远程站点证书、证书链、剩余天数都能拿下来。

这篇文章就用实际命令串一遍“openssl 显示证书有效期信息”这个主题,覆盖本地文件查询、远程服务器抓取、脚本自动化巡检和常见报错处理。命令都是可以直接复制的写法,重点会放在每个参数为什么这么用,以及哪些地方容易踩坑。

1. 证书有效期:先搞清楚要查什么

1.1 notBefore 与 notAfter 从哪来

每一张符合 X.509 标准的证书,内部都有一个 Validity 字段,里面包含两个时间:notBefore 和 notAfter。语义很直白:notBefore 表示这张证书从哪个时间点开始生效,notAfter 表示它到哪个时间点失效。在 notBefore 之前和 notAfter 之后,证书都处于不可用状态。

这个字段是 CA 签发证书时写入的,而且用的是绝对时间。客户端在验证证书时会拿“当前时间”跟这个区间做比对,超出区间就直接判过期。这也解释了为什么证书有效期检查是所有 HTTPS 排障的起点:只要时间对不上,后面的信任链、签名验证做得再完整,最终结果也是失败。实际业务里,很多证书的有效期故意越签越短,比如免费证书服务默认就只签 90 天,目的就是逼着自动化续期成为常态。所以“显示证书有效期信息”不是查一次就完了,是需要定期反复做的运维动作。

1.2 openssl 的时间格式与“双空格”陷阱

openssl 查询有效期时统一用 GMT 输出,格式长这样:

notBefore=Jan 1 00:00:00 2024 GMT notAfter=Dec 31 23:59:59 2024 GMT

先记住一个关键点:这里永远是 GMT,不是本地时间,也不是 UTC 的别名问题。GMT 和 UTC 在证书场景里可以直接等价看待,关键是 ops 人员做时间换算时心里要有这根弦。

另一个坑在“双空格”。月份后面如果是个位数的日期,比如 1 号,输出会是Jan 1,中间有两个空格,因为 openssl 用固定宽度来对齐日期字段。如果你写脚本时用简单的按空格分割去取日期,很可能把字段切错。最稳妥的取值方式不是按空格切,而是用cut -d= -f2直接取等号后面的完整字符串,从根上绕开这个坑。

2. 本地证书有效期查询:x509 子命令实操

2.1 一条命令看全:-dates 与 -noout

本地证书文件查询最常用的就是 x509 子命令,直接给文件路径:

openssl x509 -in server.crt -noout -dates

-in指定证书文件,-noout的含义是“不输出证书本身的 base64 内容”,只打印你要求的那几个字段。如果没有-noout,x509 默认会把整张证书以 PEM 格式打到标准输出,几十行 base64 字符混在一起,有效期信息反而不显眼。

这条命令输出两行,就是前面说的 notBefore 和 notAfter。实际排障中我一般还会顺手加个-subject,确认自己看的不是隔壁那张证书:

openssl x509 -in server.crt -noout -subject -dates

如果你手上的 PEM 文件是包含多张证书的 bundle(比如 fullchain),x509 默认读取的是文件里的第一张证书。正规部署里第一张通常就是站点自己的叶子证书,所以大多数情况下没问题,但心里要清楚这一点,别把中间证书当叶子证书查。

2.2 只看结束时间:-enddate、-startdate

有些场景只需要 notAfter 一个值,比如写脚本判断“还剩几天”。这时候可以用-enddate:

openssl x509 -in server.crt -noout -enddate

输出一行notAfter=...,干净利落。对应的还有-startdate,查证书开始生效时间用,实际用得少,但偶尔需要确认一张证书是不是“还没到生效时间”时很好使。注意-startdate和-enddate可以和-dates混着用,但不能互相替代,因为它们本身是独立选项。

这个写法的价值在脚本里体现得最明显:只需要一行状态,用grep notAfter过滤再cut -d=取值,比处理两行输出简单得多。

2.3 脚本友好:-checkend 直接返回结果

如果你不想自己算时间戳,openssl 还提供了一个现成的检查开关:

openssl x509 -in server.crt -noout -checkend 604800 echo $?

-checkend后面的数字是秒数。它检查的是“这张证书是否会在 N 秒内过期”,如果会,返回非 0 退出码,如果不会,返回 0。上面的例子是查 604800 秒,也就是 7 天。把数字改成 0,就变成“是否已经过期”的判断。

这个选项最大的好处是不需要做任何日期解析,直接看退出码就行。写进脚本里非常舒服:

if openssl x509 -in server.crt -noout -checkend 2592000; then echo "证书在30天内不会过期" else echo "证书即将过期或已过期,需要处理" fi

30 天对应 2592000 秒。用-checkend做快速巡检,省时省力,尤其适合在部署脚本里当预检步骤。

2.4 其他格式:DER、PFX 里的证书怎么查

不是所有证书文件都是 PEM 文本格式。Windows 系统导出的.cer、.der常见是二进制 DER 格式,直接拿 x509 读会报错。这时候加-inform DER:

openssl x509 -in cert.der -inform DER -noout -dates

还有一种常见场景是拿到一个.pfx或.p12文件,里面是证书和私钥打包在一起。先把它转成 PEM,再查有效期:

openssl pkcs12 -in cert.pfx -clcerts -nokeys -out /tmp/cert.pem openssl x509 -in /tmp/cert.pem -noout -dates

这里两个参数值得说:-clcerts表示只导出证书,不导出 CA 证书,避免导出结果里混进多余证书;-nokeys表示不要导出私钥,这样即使临时文件泄露,风险也可控。整个过程会提示输入 PFX 密码,这是文件自身的保护机制,不是 openssl 的问题。

3. 远程服务器证书有效期检查:s_client 实战

3.1 连接远程主机并提取证书日期

查本地文件容易,真正常用的是直接查线上域名证书还剩多久。这要借助 s_client 子命令,它相当于用命令行的方式发起一次 TLS 握手。最经典的写法是:

timeout 5 openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null | openssl x509 -noout -dates

整条命令拆开看:s_client 负责完成与目标服务器的 TLS 握手,并把服务器返回的证书信息输出到标准输出;后面的openssl x509 -noout -dates从这段输出里读取第一张 PEM 证书并打印有效期。</dev/null是给 s_client 提供标准输入,让它在握手完成后能正常结束;2>/dev/null把握手过程的 error 输出丢掉,免得干扰结果。

输出和查本地文件完全一样,两行 GMT 日期。这条命令就是我日常检查线上证书的主力,配一个域名变量就能在循环里批量跑。

3.2 SNI 参数不能省

很多人在远程查证书时会遇到一个怪现象:明明域名解析指向某个 IP,抓出来的证书却不是自己站点的,导致有效期信息完全对不上。原因大概率是没带-servername。

现代 Web 服务普遍用 SNI 做虚拟主机,同一个 IP 上可以部署无数个域名,服务器根据客户端 TLS 握手时携带的域名来选择返回哪张证书。如果你的连接只给了 IP 或 IP:端口,服务器就只能返回一个默认证书,通常是站点的默认页证书,甚至可能是别的域名。加了-servername www.example.com,客户端才会在握手时声明自己要访问的域名,服务器才会返回对应的证书。

所以哪怕是直接连 IP 检查,也建议写成:

timeout 5 openssl s_client -connect 203.0.113.10:443 -servername www.example.com </dev/null 2>/dev/null | openssl x509 -noout -dates

这不只是习惯问题,是能不能拿对证书的前提。

3.3 稳定性处理:超时、空输入与常见坑

s_client 命令有个烦人的特点:它会一直等到 TLS 会话结束才退出,而有些服务器在握手后不主动关闭连接,命令就会挂在那里。所以远程查询一定要用timeout包一层,5 秒足够完成握手和证书获取:

timeout 5 openssl s_client ...

输入重定向上,</dev/null比echo |更稳。echo |会送一个空行再关闭输入,大多数情况下没问题,但有些环境下会触发额外的读写延迟;</dev/null直接给 EOF,语义更干净。实测下来挂起概率低很多。

OpenSSL 1.1.1 以上的版本还多了个-brief模式,输出非常简洁,适合快速看连接状态:

timeout 5 openssl s_client -brief -connect www.example.com:443 -servername www.example.com </dev/null 2>&1

它会显示协议版本、加密套件、对端证书 CN、签名算法和验证结果,但注意它不直接显示有效期。和 x509 管线配合,一个看状态,一个看日期,互补。

3.4 证书链上每张证书的有效期怎么查

默认情况下,s_client 只显示服务器自己那张证书。如果想看整条证书链,加-showcerts,输出里会按顺序包含每一级证书,叶子在最前。

检查证书链上每一张证书的有效期,最实用的做法是把输出存成文件,然后用 awk 拆出每一张证书再逐个查:

timeout 5 openssl s_client -connect www.example.com:443 -showcerts -servername www.example.com </dev/null 2>/dev/null > chain_output.txt awk 'BEGIN{n=0} /BEGIN CERTIFICATE/{n++} {print > ("cert_" n ".pem")}' chain_output.txt for f in cert_*.pem; do openssl x509 -in "$f" -noout -subject -dates done

为什么要关心链上每一环?因为即使叶子证书没过期,中间证书过期或者没被正确下发,客户端依然无法构建完整信任链,浏览器照样报错。这种情况下只看叶子证书的剩余天数是查不出问题的。

4. 有效期监控:从手工查询到自动巡检

4.1 日期字符串转时间戳

脚本要判断“还剩多少天”,必须先把 openssl 输出的日期字符串转成时间戳。GNU date 在 Linux 上很直接:

expiry="Sep 30 23:59:59 2025 GMT" expiry_ts=$(date -d "$expiry" +%s)

但 macOS 自带的 BSD date 不支持这种写法,得用-j -f指定格式解析。这里双空格坑又来了,所以先用tr -s ' '压缩空格,再用%b %d解析:

expiry_ts=$(date -j -f "%b %d %H:%M:%S %Y %Z" "$(echo "$expiry" | tr -s ' ')" +%s)

无论哪种系统,最后拿到的expiry_ts都是 epoch 秒数,跟当前时间戳相减、除以 86400,剩余天数就出来了。转换成时间戳的最大好处是彻底绕开时区问题:epoch 是绝对时间,和你在哪个时区看它没有关系。

4.2 一个可用于生产的巡检脚本

下面是我顺手整理的一个脚本,同时支持本地证书文件和远程域名,参数直接跟在后面就行:

#!/usr/bin/env bash # cert_check.sh - 证书有效期巡检脚本 # 用法: ./cert_check.sh [证书文件或域名] [证书文件或域名] ... check_expiry_days() { local target="$1" local dates expiry expiry_ts now_ts days_left if [ -f "$target" ]; then dates=$(openssl x509 -in "$target" -noout -dates) else dates=$(timeout 5 openssl s_client -connect "${target}:443" -servername "$target" </dev/null 2>/dev/null | openssl x509 -noout -dates) fi expiry=$(echo "$dates" | grep '^notAfter' | cut -d= -f2) if [ -z "$expiry" ]; then echo "[错误] $target 无法获取有效期信息" return 1 fi expiry_ts=$(date -d "$expiry" +%s) now_ts=$(date +%s) days_left=$(( (expiry_ts - now_ts) / 86400 )) echo "目标: $target" echo " 到期时间(GMT): $expiry" echo " 剩余天数: ${days_left}天" if [ "$days_left" -le 30 ]; then echo " [警告] 剩余不足30天,需要尽快续期" fi } for target in "$@"; do check_expiry_days "$target" done

这个脚本没有用任何高级特性,核心就是“取 notAfter → 转时间戳 → 减当前时间 → 得到天数”。30 天预警阈值是我试下来比较舒服的节奏:免费证书 90 天有效期,剩下 30 天提醒一次;商业 CA 的一年证书,30 天预警也足够留出续期和重新部署的时间。注意 macOS 上要把date -d换成前面给的那条 BSD 写法。

4.3 定时任务与告警配置

巡检脚本放在一台能访问目标服务的机器上,用 crontab 定时跑。比如每天早上 9 点查一次:

0 9 * * * /opt/scripts/cert_check.sh www.example.com api.example.com >> /var/log/cert_check.log 2>&1

cron 里有一个经典问题:它会用最小化的环境变量,PATH 里可能没有 openssl,任务静默失败。所以脚本里最好写上解释器的绝对路径,或者把/usr/bin加进 PATH。日志一定要落盘,否则某天告警没触发你都不知道巡检到底跑没跑。

告警通道按团队习惯来,不管是即时通讯机器人的 webhook,还是邮件脚本,还是把日志交给监控系统去匹配关键字都可以。重要的是把“剩余天数”这个数字真正推到负责续期的人面前,而不是打印在某个没人看的终端里。

5. 常见问题与排查技巧实录

5.1 s_client 卡住、无输出

远程查询最常遇到的就是命令挂住不动。原因无非三种:网络对目标端口访问不通、服务器握手后不主动关闭连接、中间防火墙丢包导致连接处于半开状态。处理方式就是前面反复强调的timeout 5,把整个 s_client 包起来,超过时间直接掐掉。如果加了 timeout 后命令几秒内就超时退出,但输出是空,那基本可以断定是网络层的问题,先用telnet或nc去测目标端口通不通,再看往上的 TLS 层。

另一个“无输出”的场景是目标端口根本不是 TLS 服务,比如你用 443 连了一个只跑 HTTP 的后端,s_client 会一直等 ServerHello,最终超时。这种时候优先确认端口协议是否匹配。

5.2 “unable to load certificate” 的处理

本地查询最常见报错是:

unable to load certificate

原因通常是三种:文件路径不存在、文件是 DER 二进制格式、文件虽然是 .crt 后缀但内容被压缩过。路径问题用ls -l先确认真实路径;格式问题加-inform DER再试;压缩文件先解压再查。另外,有些证书导出时会带 Windows 行尾符,理论上 openssl 能容忍,但如果你看到莫名其妙的解析失败,可以用dos2unix处理一下再试。

还有一点容易被忽略:如果文件是 PFX/P12 格式,你不能直接丢给 x509,必须先像 2.4 节那样转换成 PEM。openssl 的每个子命令对输入格式都有明确要求,报错不是工具坏了,是格式不对。

5.3 证书“看着没过期”,验证却报过期

这种是最耗时的排障场景。s_client 输出里出现:

verify error:num=10:certificate has expired

但你自己看 notAfter,明明还有几个月。此时要怀疑的不是这张证书,而是整个验证链路。

首先要确认你这台机器的系统时间是否正确。TLS 验证用的是本机当前时间,如果服务器时钟没同步,差了几天甚至几个月,客户端就会把未过期的证书判成过期。养成 NTP 同步的习惯能避免大量这种问题。

其次看证书链是否完整。叶子证书没过期,但中间证书已经过期,或者服务器没有下发完整的链,客户端在验证中间层时同样会报过期相关错误。用 3.4 节的方法把链上每张证书都看一遍,往往就能定位到真正过期的环节。

5.4 时间换算与本地时区

自己写脚本算剩余天数时,最容易栽在时区换算上。假设你在东八区,本地时间是 2025 年 1 月 1 日早上 8 点,而证书的 notAfter 是Dec 31 23:59:59 2024 GMT,如果直接把本地时间拿去比,会误判为“已经过期了一个晚上”,但其实 GMT 时间还没到那一刻。

正确做法永远是:先把到期时间转成 epoch 秒数,再用date +%s取当前 epoch,两者相减。epoch 与所在时区无关,任何机器上算出来的结果一致。另一个小提醒:cut -d=取值时不要顺手 trim 掉尾部空格,openssl 在固定宽度字段后有空格,这是正常的,date 解析时会自动忽略。

5.5 错误信息速查表

日常遇到的报错和处理方式,整理成一张表方便直接对着查:

现象常见原因处理方式
unable to load certificate文件不是 PEM、路径错误、格式为 DER确认路径,加-inform DER
s_client 长时间不退出服务器未关闭连接、网络不通用timeout 5包一层
no peer certificate available端口不是 TLS、SNI 未匹配确认端口协议,加-servername
verify error:num=10证书已过期或系统时间不对查看 notAfter,检查 NTP
verify error:num=20缺少中间证书或本地缺信任锚检查证书链完整性,配置-CAfile
date: invalid dateGNU date 无法解析格式先用tr -s ' '压缩双空格
时间戳算出来为负时区混用导致比较错误统一通过 epoch 秒数比较

最后分享一个我自己的习惯:每次看到一张证书的有效期时,不只是记下 notAfter,还要顺手看下 subject 和 SAN(-text里能看到)。因为排障时真正的问题经常不是“过期了”,而是“拿错证书了”。域名好几个、证书好几张,贴到同一台服务器上,查有效期时先确认这张确实是目标域名的证书,省的后面白折腾。证书有效期检查这事,工具用熟只是第一步,把检查变成定时动作、把告警变成看得见的提醒,才是真正避开线上事故的关键。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进