在当今互联网业务中,HTTPS 已经成为全网 Web 服务与 API 接口的绝对标配。然而,无论是初创团队还是大型互联网企业,因 SSL/TLS 证书过期未及时续签而导致的服务不可用、浏览器强拦截以及用户信任危机等生产事故屡见不鲜。
人工记忆和表格记录往往存在遗漏风险,最可靠的防范机制是:建立自动化巡检与监控告警体系,当证书剩余有效期不足 30 天时自动触发企业微信/钉钉/邮件告警。
本文深度解析利用原生 openssl 命令行远程探测 HTTPS 证书剩余天数的核心原理,并提供单域名提取、批量巡检、多并发加速及监控系统集成的完整工程落地方案。
一、OpenSSL 探测原理与核心参数速查
无需登录部署证书的目标服务器,仅凭客户端即可通过 TLS 握手直接获取目标域名的公开证书信息。核心命令流水线如下:
echo | openssl s_client -servername <域名> -connect <域名>:443 2>/dev/null | openssl x509 -noout -enddate
核心参数解析对照表
| 核心组件 / 参数 | 功能描述 | 生产必备原因 |
|---|---|---|
echo \| |
向客户端输入一个换行符 | s_client 默认保持交互式等待,输入换行可使其在获取证书后主动断开退出 |
timeout 5 |
强制设定连接超时上限 | 避免目标网络不可达或握手阻塞导致脚本永远挂起卡死 |
-servername |
启用 SNI (Server Name Indication) | 至关重要! 现代服务器一个 IP 往往绑定多个域名,不加此参数会导致握手返回默认泛域名证书 |
-connect |
指定探测目标的主机名与端口 | 默认 Web 端口为 443 |
2>/dev/null |
屏蔽底层握手详细过程与错误日志 | 过滤冗余调试信息,保持标准输出纯净 |
openssl x509 |
X.509 证书专用解析工具 | 专门从流中抓取证书体并进行结构化解码 |
-noout -enddate |
仅输出证书截止时间 notAfter |
屏蔽证书公钥 Base64 编码,只返回格式如 notAfter=Dec 31 23:59:59 2026 GMT |
[!WARNING] SNI 陷阱警告: 很多开发者在写检测脚本时习惯只写
-connect $domain:443,而遗漏了-servername $domain。在反向代理(如 Nginx / CDN / K8s Ingress)承载多租户或多证书场景下,缺少 SNI 会导致拿到的证书与目标域名不匹配,造成严重的误报或漏报!
二、单域名秒级提取一行流实战
利用 openssl 获取到证书的截止时间字符串后,需要将其换算为 UNIX 时间戳,并与当前时间戳对比计算剩余天数。
1. Linux (GNU date) 一行流
在主流 Linux 发行版(CentOS, Ubuntu, Debian, Alpine 等)上,date -d 支持直接解析 GMT 时间格式:
DOMAIN="www.baidu.com"
# 1. 抓取截止时间字符串 (格式: notAfter=May 17 06:48:47 2026 GMT)
END_DATE=$(echo | timeout 5 openssl s_client -servername "$DOMAIN" -connect "$DOMAIN":443 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d '=' -f 2)
# 2. 将截止时间与当前时间转换为秒级时间戳并计算天数
END_TIMESTAMP=$(date +%s -d "$END_DATE")
NOW_TIMESTAMP=$(date +%s)
DAYS_LEFT=$(( (END_TIMESTAMP - NOW_TIMESTAMP) / 86400 ))
echo "域名 $DOMAIN 证书剩余天数: $DAYS_LEFT 天"
2. macOS (BSD date) 跨平台兼容
macOS 原生使用的 BSD date 不支持 -d 参数,需使用 -j -f:
# macOS 兼容提取时间戳
END_TIMESTAMP=$(date -j -f "%b %d %T %Y %Z" "$END_DATE" +%s)
三、批量多域名巡检脚本实现 (基础串行版)
在日常运维中,我们通常将需要监控的域名清单单独存放在一个配置文件中,便于日常扩容与解耦维护。
1. 域名清单配置文件 domain_list.txt
www.baidu.com
www.github.com
api.x7lab.cn:443
2. 巡检脚本 check_ssl_expire.sh
#!/usr/bin/env bash
# check_ssl_expire.sh - 批量检测 SSL 证书过期剩余天数
DOMAINS_FILE="${1:-domain_list.txt}"
WARN_DAYS=30
if [ ! -f "$DOMAINS_FILE" ]; then
echo "错误: 域名清单文件 $DOMAINS_FILE 不存在!"
exit 1
fi
echo "==================== SSL 证书有效期巡检报告 ===================="
printf "%-30s | %-12s | %-10s\n" "域名" "剩余天数" "状态"
echo "----------------------------------------------------------------"
while IFS= read -r line || [ -n "$line" ]; do
# 过滤注释与空行
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
# 拆分域名与端口 (默认 443)
DOMAIN=$(echo "$line" | cut -d ':' -f 1 | xargs)
PORT=$(echo "$line" | cut -s -d ':' -f 2 | xargs)
PORT="${PORT:-443}"
# 抓取证书过期截止时间
RAW_DATE=$(echo | timeout 5 openssl s_client -servername "$DOMAIN" -connect "$DOMAIN:$PORT" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d '=' -f 2)
if [ -z "$RAW_DATE" ]; then
printf "%-30s | %-12s | %-10s\n" "$DOMAIN:$PORT" "探测失败" "ERROR"
continue
fi
# 计算剩余天数
END_TS=$(date +%s -d "$RAW_DATE")
NOW_TS=$(date +%s)
DAYS=$(( (END_TS - NOW_TS) / 86400 ))
if [ "$DAYS" -le "$WARN_DAYS" ]; then
STATUS="⚠️ 紧急临期"
else
STATUS="✅ 正常"
fi
printf "%-30s | %-12s | %-10s\n" "$DOMAIN:$PORT" "${DAYS} 天" "$STATUS"
done < "$DOMAINS_FILE"
四、并发加速巡检与进程控制 (进阶健壮版)
当监控列表中的域名多达数十上百个时,串行检测如果遇到网络握手超时的域名,整体执行时间会大幅延长。因此引入后台并发子进程 + wait 同步屏障是极佳的提速手段。
[!TIP] 并发安全性建议: 避免使用硬编码
sleep盲等,改用 Bash 原生wait指令等待所有后台子进程执行收敛,既保证数据完整性,又兼顾最高执行速度。
#!/usr/bin/env bash
# check_ssl_concurrent.sh - 多并发高性能证书过期巡检
DOMAINS_FILE="${1:-domain_list.txt}"
TEMP_RESULT=$(mktemp /tmp/ssl_check_XXXXXX.txt)
WARN_DAYS=30
# 单域名检测函数
check_domain() {
local domain=$1
local port=${2:-443}
local raw_date
raw_date=$(echo | timeout 3 openssl s_client -servername "$domain" -connect "$domain:$port" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d '=' -f 2)
if [ -z "$raw_date" ]; then
echo "$domain:$port|FAILED|0" >> "$TEMP_RESULT"
return
fi
local end_ts now_ts days
end_ts=$(date +%s -d "$raw_date")
now_ts=$(date +%s)
days=$(( (end_ts - now_ts) / 86400 ))
echo "$domain:$port|SUCCESS|$days" >> "$TEMP_RESULT"
}
# 并发拉起巡检进程
while IFS= read -r line || [ -n "$line" ]; do
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
domain=$(echo "$line" | cut -d ':' -f 1 | xargs)
port=$(echo "$line" | cut -s -d ':' -f 2 | xargs)
port="${port:-443}"
check_domain "$domain" "$port" &
done < "$DOMAINS_FILE"
# 优雅等待所有后台并发子任务完成
wait
# 格式化输出汇总
echo "==================== 并发巡检汇总结果 ===================="
while IFS='|' read -r target status days; do
if [ "$status" = "SUCCESS" ]; then
if [ "$days" -le "$WARN_DAYS" ]; then
echo -e "[\033[31m告警\033[0m] $target 仅剩 $days 天,请立即续签!"
else
echo -e "[\033[32m正常\033[0m] $target 剩余 $days 天"
fi
else
echo -e "[\033[33m失败\033[0m] $target 证书探测失败,请检查网络或端口"
fi
done < "$TEMP_RESULT"
# 清理临时文件
rm -f "$TEMP_RESULT"
五、企业级监控告警集成方案
将脚本输出与主流监控平台联动,才能真正实现无人值守:
1. Zabbix 自动化发现与低级别发现 (LLD)
- 将脚本部署为 Zabbix Agent 的
UserParameter:text UserParameter=ssl.cert.expiry[*],/usr/local/bin/check_ssl_single.sh $1 - 在 Zabbix Server 上配置触发器表达式:
last(/host/ssl.cert.expiry[{#DOMAIN}]) < 30,级别设为 High。
2. Prometheus Node Exporter Textfile 采集
- 定时将巡检结果输出为 Prometheus 标准 Metrics 格式并落盘到
/var/lib/node_exporter/textfile_collector/ssl_expiry.prom:text # HELP ssl_cert_days_left Days until SSL certificate expires # TYPE ssl_cert_days_left gauge ssl_cert_days_left{domain="api.x7lab.cn"} 78 ssl_cert_days_left{domain="www.baidu.com"} 221
3. Crontab 定时企业微信 / 钉钉机器人 Webhook 告警
- 配置每日早晨 9:00 执行一次巡检脚本,当
DAYS <= 30时,调用 Webhook 发送 Markdown 格式警报卡片直达运维群。
六、总结与最佳实践
- 务必带上
-servername参数:确保 SNI 扩展生效,避免命中多虚拟主机中的默认证书; - 连接保护设置
timeout:防止遇到防火墙丢包导致巡检脚本无限期悬挂阻塞; - 并发任务使用
wait控制:抛弃随意sleep的不确定性,确保所有子进程完整收拢后再读盘输出; - 双重防御原则:除外部黑盒探测外,核心入口(如 Nginx / Traefik / ACME 客户端)还应配置白盒自动续签(如 Certbot / Let's Encrypt),做到自动续签为主、远程巡检兜底。