证书只剩 7 天才发现?一套可复现的 HTTPS 安全配置巡检方案

简介: SSL 证书检测的价值不在“查一次”,而在“变成常态化的基线”。把有效期、协议版本、证书链、主机名、跳转与 HSTS 这几项固化成脚本的退出码,再挂上定时调度,证书过期就从“事故”降级为“工单”。托管服务负责证书本身的签发与提醒,自建脚本负责配置细节的兜底,两者不是替代关系。

image.png
证书过期不是“慢慢失效”,而是到点直接失效:浏览器整页拦截、App 内 H5 白屏、小程序请求全部失败、第三方回调因 TLS 校验失败被丢弃。更隐蔽的是配置问题——证书链没发全、还开着 TLS 1.0、HSTS 没配、证书域名与访问域名不匹配,这些在桌面 Chrome 上往往"看起来正常",却在 Android 旧版本、Java 8 客户端、IoT 设备上直接握手失败。

本文给出一套可直接落地的巡检方法:用 openssl + curl 写一个可复现的探测脚本,覆盖有效期、协议版本、证书链、主机名校验、HTTP 跳转、HSTS、OCSP Stapling 七个维度,再说明如何把它挂到定时任务或阿里云函数计算上做周期性巡检,并与阿里云数字证书管理服务的证书生命周期能力配合使用。

一、场景与目标

典型触发场景:

  • 团队用免费证书或商业证书,但续期靠人肉记日历,证书数量超过 10 张后必然漏;
  • 站点迁到 CDN、负载均衡 SLB、应用型负载均衡 ALB、Web 应用防火墙 WAF 之后,证书要在边缘节点和源站分别维护,容易出现“边缘换了、源站没换”;
  • 等保、密评或客户安全问卷要求提供“TLS 配置合规”的证明材料,但没人能说清当前到底开了哪些协议套件。

本文的目标不是做漏洞扫描,而是做配置基线巡检:对一组域名周期性输出结构化结果,证书剩余天数低于阈值、或出现禁止的协议版本时能触发告警。

二、前置条件与环境

项 要求
执行环境 Linux(GNU coreutils 的 date -d 依赖),或任意装有 openutils 的容器
OpenSSL 建议 1.1.1 及以上(支持 TLS 1.3 与 -verify_hostname);openssl version 确认
curl 7.29 及以上,需要 -w '%{http_code}' 与 %{redirect_url}
网络 执行机能直连目标 443/tcp 与 80/tcp;不要从被 WAF 拦截的出口发起高频探测
授权 只巡检自有或已获书面授权的域名;脚本仅做 TLS 握手与 HTTP HEAD 请求,不含任何攻击载荷

注意一个前提:审计对象是域名,不是 IP。脚本必须带 -servername(SNI),否则在共享 IP 的 CDN 或 SLB 上会拿到默认证书,结论完全错误。

三、方案选择:三条路线的取舍

路线 A:本地脚本 + cron/systemd timer。 成本最低、完全可控、结果可入自有监控;缺点是执行机单点,跨地域探测能力弱。

路线 B:阿里云函数计算(Function Compute)+ 定时触发器。 无需维护执行机,天然适合多域名批量巡检,结果可写入日志服务 SLS 并配置告警。适合域名数量在几十到几百、需要固定周期执行的场景。需注意:函数默认的公网访问能力与是否配置 VPC 有关,若目标在 VPC 内网,需要提前规划 NAT 网关等出口,具体以官方文档为准。

路线 C:托管能力组合。 数字证书管理服务负责证书签发、托管与到期提醒,云监控的站点监控负责可用性与响应时间探测。这类能力开箱即用,但探针维度和告警粒度受产品边界约束,证书链完整性、协议版本矩阵这类细节仍建议用脚本自查。是否支持特定云产品的证书部署、提醒提前天数与渠道,请以控制台和官方文档为准。

实践中的常见组合是:C 管证书生命周期,A 或 B 管配置基线,两者结果交叉比对。

四、实施步骤

  1. 梳理资产:把对外提供 HTTPS 的域名列全,包括主域名、API 子域、静态资源域、回调域。特别注意 CDN 加速域名和 WAF 防护域名,它们与源站证书是两套。
  2. 确定基线:建议基线为——TLS 1.2 / 1.3 开启,TLS 1.0 / 1.1 关闭;证书链完整;主机名校验通过;HTTP 301/308 跳转到 HTTPS;HSTS 至少 max-age=31536000;剩余天数阈值 30 天(免费 90 天证书可设 21 天)。
  3. 单域名验证:先用脚本跑通一个域名,人工核对结论与浏览器开发者工具的安全面板是否一致。
  4. 批量巡检:用 xargs -P 并发,控制并发度在 4~8,避免触发 WAF 频控。
  5. 接告警:脚本用退出码表达状态(0 正常 / 1 告警 / 2 探测失败),由外层调度器判断并推送。
  6. 闭环:把“证书续期”和“配置变更”纳入变更流程,续期后在边缘节点与源站两侧都复跑一次。

五、关键配置与代码

以下脚本可直接保存为 ssl_audit.sh,chmod +x 后执行 ./ssl_audit.sh www.example.com。

#!/usr/bin/env bash
# ssl_audit.sh —— 单域名 HTTPS 配置巡检
# 用法: ./ssl_audit.sh <host> [port]
# 退出码: 0 正常 / 1 存在告警项 / 2 探测失败(握手不成功)
set -uo pipefail

HOST="${1:?usage: ssl_audit.sh <host> [port]}"
PORT="${2:-443}"
WARN_DAYS="${WARN_DAYS:-30}"
TIMEOUT=10

SCLIENT=(openssl s_client -connect "${HOST}:${PORT}" -servername "${HOST}")
fail=0
emit() {
    printf '%-24s %s\n' "$1" "$2"; }

# ---------- 1. 握手 + 证书有效期 ----------
cert=$(echo | "${SCLIENT[@]}" 2>/dev/null | openssl x509 -noout -subject -issuer -dates 2>/dev/null)
if [ -z "$cert" ]; then
  emit "tls_handshake" "FAIL"
  exit 2
fi
emit "tls_handshake" "OK"
emit "subject" "$(echo "$cert" | awk -F'=' '/^subject/{print $2}')"
emit "issuer"  "$(echo "$cert" | awk -F'=' '/^issuer/{print $2}')"

end_date=$(echo "$cert" | awk -F'=' '/^notAfter/{print $2}')
end_ts=$(date -u -d "$end_date" +%s)
days=$(( (end_ts - $(date -u +%s)) / 86400 ))
emit "days_remaining" "$days"
[ "$days" -lt "$WARN_DAYS" ] && fail=1

# ---------- 2. 协议版本 ----------
probe_proto() {
   
  out=$(echo | "${SCLIENT[@]}" "-$1" 2>/dev/null)
  if [ -z "$out" ] || echo "$out" | grep -q "Cipher is (NONE)"; then echo "OFF"; else echo "ON"; fi
}
for v in tls1 tls1_1 tls1_2 tls1_3; do
  s=$(probe_proto "$v")
  emit "proto_$v" "$s"
  # 基线:1.0/1.1 必须 OFF,1.2/1.3 至少一个 ON
  case "$v" in
    tls1|tls1_1) [ "$s" = "ON" ] && fail=1 ;;
  esac
done

# ---------- 3. 证书链完整性 ----------
chain_cnt=$(echo | "${SCLIENT[@]}" -showcerts 2>/dev/null | grep -c "BEGIN CERTIFICATE")
emit "chain_certs" "$chain_cnt"   # 通常 >=2(叶子 + 中间 CA)
[ "$chain_cnt" -lt 2 ] && fail=1

# ---------- 4. 链校验 + 主机名匹配 ----------
verify=$(echo | "${SCLIENT[@]}" -verify_return_error -verify_hostname "$HOST" 2>&1 \
         | grep -i "Verify return code" | head -n1)
emit "verify" "${verify:-UNKNOWN}"
echo "$verify" | grep -q "0 (ok)" || fail=1

# ---------- 5. HTTP -> HTTPS 跳转 ----------
code=$(curl -sS -o /dev/null -m "$TIMEOUT" -w '%{http_code}' "http://${HOST}/" 2>/dev/null || echo 000)
emit "http_code" "$code"
case "$code" in
  301|308) ;;
  *) fail=1 ;;
esac

# ---------- 6. HSTS ----------
hsts=$(curl -sSI -m "$TIMEOUT" "https://${HOST}/" 2>/dev/null \
       | awk -F': ' 'tolower($1)=="strict-transport-security"{print $2}' | tr -d '\r')
emit "hsts" "${hsts:-MISSING}"
[ -z "$hsts" ] && fail=1

# ---------- 7. OCSP Stapling ----------
ocsp=$(echo | "${SCLIENT[@]}" -status 2>/dev/null | grep -A1 "OCSP Response Status" | tail -n1 | tr -d ' ')
emit "ocsp_stapling" "${ocsp:-NONE}"

exit "$fail"

批量执行与结果留存:

mkdir -p out
cat > domains.txt <<'EOF'
www.example.com
api.example.com
static.example.com
EOF

# GNU xargs,并发 4
xargs -a domains.txt -P 4 -I{
   } sh -c \
  './ssl_audit.sh "{}" > "out/{}.log" 2>&1; echo "{} => exit=$?"'

若要接入函数计算,把上面的逻辑改写为 Python(ssl 模块取 getpeercert() 的 notAfter,socket 指定 ssl.PROTOCOL_TLS_CLIENT 并按版本构造上下文),配置定时触发器(Cron 表达式),将结果以 JSON 行写入日志服务 SLS,再对 days_remaining < 30 或 proto_tls1 == ON 配置日志告警。注意函数执行超时要覆盖全部域名的串行耗时,必要时在函数内并发。

六、验证结果

单域名输出形如下面(仅为字段格式示意,不代表任何真实站点数据):

tls_handshake            OK
subject                  CN=www.example.com
issuer                   C=US, O=Example CA, CN=Example RSA DV
days_remaining           76
proto_tls1               OFF
proto_tls1_1             OFF
proto_tls1_2             ON
proto_tls1_3             ON
chain_certs              2
verify                   Verify return code: 0 (ok)
http_code                301
hsts                     max-age=31536000
ocsp_stapling            successful

人工核对方法:

  • 证书指纹与到期时间,和浏览器开发者工具 → 安全 → 查看证书 中的结果比对;
  • 协议版本,可用 openssl s_client -connect host:443 -servername host -tls1_1 手工复现,确认返回的是握手失败而非空输出;
  • 证书链,把 -showcerts 的输出另存为 .pem,用 openssl verify -untrusted chain.pem leaf.pem 复核;
  • 若脚本结论与浏览器不一致,优先怀疑 SNI 缺失、中间有代理做了 TLS 卸载、或本地系统时间偏差。

七、成本、性能与安全注意事项

性能。 单域名全量探测约需 6~8 次 TLS 握手加 2 次 HTTP 请求,量级在百毫秒到秒级,取决于网络 RTT。100 个域名串行执行可能达到分钟级;并发 4~8 通常可压到十几秒。但并发不要盲目拉高:一方面会触发 WAF 的 CC 防护,另一方面部分 SLB 或 CDN 实例对握手速率有保护策略。

成本。 本地 cron 方案只消耗执行机资源。函数计算按调用次数与资源使用量计费,是否落在免费额度内取决于你的调用量与规格,请以官方计费文档为准。日志服务 SLS 按存储量与读写流量计费,注意给结果设置合理的保留周期。

安全。

  1. 脚本结果含证书主体与颁发者信息,属于资产指纹,日志不要无鉴权公开;
  2. 不要在脚本里硬编码 AK/SK,函数计算场景使用角色授权而非在代码中放置长期凭证;
  3. 巡检频率建议 1 次/小时到 1 次/天,不要做成秒级压测;
  4. 关闭 TLS 1.0/1.1 前先确认没有遗留客户端,可通过 SLS 或 Nginx access_log 中的 $ssl_protocol 变量统计真实流量分布,再灰度关闭;
  5. 开启 HSTS 前务必确认全站(含所有子域,若使用 includeSubDomains)都已 HTTPS 可用,否则回滚代价很高,必要时先用较短的 max-age 观察。

八、常见问题

Q1:脚本报握手失败,但浏览器能打开?
先加 -servername;再确认执行机出网是否被安全组或 NAT 策略拦截;最后排查是否被WAF 按 User-Agent 拦截。

Q2:chain_certs 是 1,但浏览器显示正常?
桌面浏览器会做 AIA 补链,Android 与 Java 客户端通常不会。链不全的表现是“部分客户端报 unable to get local issuer certificate”,必须在服务端把中间证书一并下发。

Q3:verify 报 unable to verify the first certificate 或主机名不匹配。
前者的常见原因是链不全或用了私有 CA;后者检查证书 SAN 是否覆盖实际访问域名,泛域名证书只覆盖一级子域。

Q4:CDN 后面怎么审计?
以加速域名为审计对象。CDN 回源时的证书是另一条链路,需要单独探测源站或回源域名,不要用边缘节点的结果推断源站配置。

Q5:date -d 在 macOS 上报错。
GNU 与 BSD date 参数不同,建议在 Linux 容器内执行,或用 Python 版本替代。

Q6:为什么 proto_tls1 显示 OFF 但退出码仍是 1?
检查其他告警项——退出码是聚合结果,逐行看具体哪一项 FAIL。

Q7:数字证书管理服务里已续期,脚本还显示旧证书?
续期不等于部署。证书需要重新上传或部署到 SLB、ALB、CDN、WAF 等具体实例上,各产品的支持范围与生效时间不同,请以控制台实际状态为准,部署后复跑一次脚本确认。

九、总结

SSL 证书检测的价值不在“查一次”,而在“变成常态化的基线”。把有效期、协议版本、证书链、主机名、跳转与 HSTS 这几项固化成脚本的退出码,再挂上定时调度,证书过期就从“事故”降级为“工单”。托管服务负责证书本身的签发与提醒,自建脚本负责配置细节的兜底,两者不是替代关系。

建议读者先从 3~5 个核心域名开始跑通脚本,确认输出与浏览器一致后,再扩到全量资产并接入告警。最终请结合自己的账号权限、地域和业务环境完成验证:不同地域的产品能力、证书部署支持范围、计费方式与告警渠道都可能存在差异,以阿里云官方文档和控制台实际展示为准。

另外,我也建议定期给网站做一次诊断检查,及时发现问题,尽早解决处理,这里推荐一款我经常使用的在线诊断工具(实用工具吧-网站综合诊断),浏览器打开即可使用,免费免注册,方便快捷。
image.png

相关文章
|
25天前
|
人工智能 自然语言处理 机器人
客户满意度飙升背后:那些选对了智能客服的企业做对了什么?
AI客服体验两极分化:一边是“答非所问”“转人工难”的投诉激增,一边是94%+满意度、秒级响应的标杆实践。关键不在是否用AI,而在能否“把事办完”、实现顺畅人机协同、让服务数据反哺业务。阿里云瓴羊Quick Service以AI Agent闭环执行、情绪-复杂度动态路由、知识库智能迭代三大能力,推动客服从“能答”迈向“能干”。
|
22天前
|
机器学习/深度学习 人工智能 测试技术
Qwen3.8抢先体验!大家要心理准备了,不出意外,最新版即将发布并开源了?
Qwen3.8系列发布,开创大模型“精调度”新范式:Max版2.4T参数(仅95B激活)、Flash-Next降本90%、27B版可跑RTX显卡。四大维度解析架构革新、性能突破(PaperBench 93分)、开源普惠与落地实践,助力开发者即刻上手。
627 0
|
22天前
|
人工智能 小程序 JavaScript
用 AI 做小程序 UI 自动化测试:从搭环境到生成报告
哈喽,我是 Raina。 &#x20;之前我们分享过 Web 、APP和桌面端的 UI 自动化测试方案。后来也有不少同学问:小程序端的自动化能不能结合 AI 去实现呢? 答案是可以,而且微信官方已经提供了对应的框架: miniprogram-automator 。 这篇文章就结合一次实际落地过程,讲清楚小程序 UI 自动化怎么跑起来,以及如何借助 Skill 把“探索页面、生成脚本、执行用例、输出报告”这条链路串起来,尽量少写重复代码,让方案更容易在团队里推广。 为什么选 miniprogram-automator 小程序自动化方案有不少,但综合适配性和稳定性来看,官方的 miniprogra
171 0
|
25天前
|
弹性计算 负载均衡 网络协议
阿里云轻量应用服务器最新价格:200M峰值带宽不限流量,38元一年起
阿里云轻量应用服务器以38元/年起、200M峰值带宽、不限流量为主打,2核2G档位38元需新用户秒杀、68元免抢购,更高负载可选2核4G(379元/年)、4核8G(1159元/年)、4核16G(1599元/年)。但200M为峰值带宽而非固定独享,不限流量也不等于可无限承载高带宽业务,轻量服务器同样不适合高并发核心生产系统。选购前应确认账号资格、库存、地域、备案与续费规则,结合业务负载理性选型。
|
28天前
|
存储 人工智能 运维
阿里云 OSS 对象存储服务租用价格表(一年 / 按月 / 按小时报价明细)
阿里云OSS对象存储是海量、安全、低成本、高可靠的云存储底座,广泛用于网站图片、音视频、静态资源、备份归档、大数据、AI训练数据集等场景。OSS提供标准、低频、归档、冷归档、深度冷归档五种存储类型,计费由**存储费用、流量费用、请求费用**以及图片处理、传输加速等增值费用共同组成。支持**按量后付费**与**资源包预付费**两种模式,资源包可按月、按年购买,按量计费按小时折算实际用量。本文整理各存储类型的报价明细,拆解计费规则、选型思路,附带命令行代码示例,帮助开发者、运维人员准确测算对象存储成本。
323 0
|
并行计算 前端开发 异构计算
告别服务器繁忙,云上部署DeepSeek
本文以 DeepSeek-R1-Distill-Qwen-32B-FP8 为例,向您介绍如何在GPU实例上使用容器来部署量化的 DeepSeek-R1 蒸馏模型。
|
JavaScript Java 关系型数据库
基于springboot的小区车位租售管理系统
针对城市化进程中小区停车难问题,本文设计基于SpringBoot的车位租售管理系统,结合Vue前端与MySQL数据库,实现车位信息数字化、租售流程自动化。系统支持在线查询、申请、支付及数据统计,提升管理效率与用户体验,促进资源优化配置。
|
Devops jenkins API
DevOps 内幕:使用 Jira Automation 更智能、更快速地工作
Jira 不仅是任务管理工具,还可通过 API 与 Webhook 实现自动化,减少手动操作,提升效率。本文通过实际案例,介绍如何用 Jira 自动化简化 Git 访问流程,结合 Jenkins 实现审批后自动授权,并说明其优势与局限。
523 1
|
8月前
|
SQL 关系型数据库 MySQL
Linux 安装 mysql | 创建mysql 工具连接不上的解决办法
MySQL本地安装后远程连接失败?常见原因:防火墙未放行23306/3306端口,或root用户仅限localhost访问。需依次检查:①系统及云平台安全组端口策略;②MySQL用户权限,执行ALTER USER+CREATE USER+GRANT授权并刷新权限。(239字)
|
8月前
|
Ubuntu Linux iOS开发
给计算机新生的操作系统全景指南
资深开发工程师致大一新生的操作系统入门指南:从Windows的易用、macOS的优雅,到Linux的开源力量,详解三大系统核心特性与开发者价值,助你夯实基础、拓宽视野。