我的 dingzh.cc 和 *.dingzh.cc 证书由 1Panel 管理。本机站点会用到它,静态资源域名 alicdn.dingzh.cc 使用阿里云 CDN,博客的 dingzh.cc、www.dingzh.cc 则接入了 ESA。这些位置需要分别部署证书。

因此,我希望在一次续签之后,能够知道新证书有没有到达这些位置,以及实际访问时拿到的是哪张证书。某一步失败时要有提醒,临时网络故障恢复后也要能接着完成。

目前这套流程由 1Panel、自建 Python 脚本和 Hermes 的通知入口共同完成。申请与续签继续交给面板,脚本负责同步、核验和记录结果。这篇文章记录它们怎样配合,以及几个容易误判成功的地方。

一张证书,几个部署位置

先把当前使用范围列出来,后面的同步和检查都以它为边界:

位置当前用途更新方式
1Panel保存证书、管理 ACME 与 DNS 账户、自动续签由面板管理
本机绑定站点OpenResty 对外提供 HTTPS由 1Panel 更新站点证书并重载
传统 CDNalicdn.dingzh.cc 的访客侧 HTTPS脚本调用 CDN API
ESAdingzh.cc、www.dingzh.cc 的访客侧 HTTPS脚本更新指定的 ESA 站点证书记录

我使用的是 DNS 账户模式,证书已开启自动续签。1Panel 支持对 DNS 账户、HTTP 等申请方式启用自动续签,具体仍要看证书来源与面板中的开关状态[1]1Panel 证书续签说明参考资料。

这里的云端证书负责访客到边缘节点的连接。源站的 HTTPS 和边缘 HTTPS 各有自己的配置;我选择让这几个受管位置使用同一张证书,便于统一同步和比较。其他方案也可以为边缘与源站分别管理证书,检查时相应使用各自的目标值。

把续签、同步和每日检查接起来

所有自建文件集中在 /opt/1panel/tools/cert-sync/,统一通过 manage.py 调用。主要有两个触发入口:

入口触发时机做什么
续签后钩子1Panel 导出新证书后同步云端,再安排延迟核验
1Panel 计划任务每天 09:00,服务器东京时间检查当前状态,补做同步并处理通知
1Panel 负责续签与本机部署,自建脚本连接云端同步、核验和通知,每日任务提供补偿入口
1Panel 负责续签与本机部署,自建脚本连接云端同步、核验和通知,每日任务提供补偿入口

证书设置里启用了“推送证书到本地目录”,目标是工具目录下的 dingzh.cc/。续签后执行的自定义调用为:

1
/usr/bin/python3 -B /opt/1panel/tools/cert-sync/manage.py sync --after-renew

每日任务调用同一个入口:

1
/usr/bin/python3 -B /opt/1panel/tools/cert-sync/manage.py sync --daily

任务的表达式是 0 9 * * *,由 1Panel 自己调度。这两个入口都围绕当前证书工作;每日同步任务不会重新申请证书,续签时机仍由面板决定。

这样分工以后,ACME 账户、DNS 验证和证书签发继续沿用面板的功能,脚本只需要处理证书已经生成之后的事情。

同步前,先确定要部署哪张证书

用叶证书指纹标识一次更新

我用 SHA-256 指纹识别证书。这里比较的是 PEM 中第一张叶证书,也就是签给这些域名的那张证书;证书链仍随 fullchain.pem 一起部署。

核心代码很短:

1
2
3
4
5
6
from cryptography import x509
from cryptography.hazmat.primitives import hashes

def fingerprint(pem: str) -> str:
cert = x509.load_pem_x509_certificate(pem.encode())
return cert.fingerprint(hashes.SHA256()).hex()

同一个指纹会贯穿本次同步、本机文件检查、云端配置检查和线上 TLS 检查。文件名、证书名称和更新时间可以辅助排查,真正判断是否为同一张证书时,以指纹为准。

上传前还会检查证书与私钥的公钥是否一致、SAN 是否同时包含 dingzh.cc 和 *.dingzh.cc,以及有效期是否适合部署。当前实现拒绝上传剩余有效期不足 7 天的证书;日常健康检查则在剩余不足 14 天时报告异常,提前留出排查时间。这两个数值是本机的部署门槛和告警门槛。

防止旧导出文件覆盖云端

平时运行每日任务或手动同步时,脚本会先比较 1Panel 数据库中的证书与导出文件。如果两者指纹不同,就停止云端写入并记录异常。

否则,一份没有及时更新的导出文件也可能被当作正确来源,再次覆盖已经更新的云端证书。

续签钩子有一个特殊的时序:它执行时,导出文件已经变了,面板数据库和站点重载可能还未完成。所以 --after-renew 会按这条路径处理新导出文件,最终一致性检查留到钩子返回后完成。

只更新存在差异的目标

同步开始前,脚本先读取 CDN 和 ESA 的现有配置。已经匹配目标指纹的记录可以直接复用;只有存在差异的目标才需要更新。

传统 CDN 使用 SetCdnDomainSSLCertificate。首次上传时传入证书和私钥,使用 CertType=upload;云端已有可复用证书时,使用 CertType=cas 和 CertId[2]CDN 证书更新接口参考资料。

在我这套实现中,脚本会取得已确认的云证书 ID,再用它更新 ESA 中指定的证书记录。ESA 的 SetCertificate 接口支持通过 Type=cas、CasId 和 Region 引用云证书[3]ESA 站点证书更新接口参考资料。ESA 的证书记录 ID 与云证书 CasId 是两个字段,更新时需要分清。

写入之后还要重新查询。CDN 比较返回证书的指纹;ESA 比较 FingerprintSha256、SAN 和状态,这些字段可以从 ListCertificates 取得[4]ESA 站点证书查询接口参考资料。遇到“证书重复”一类响应,也要重新读取配置,确认它已经指向目标证书,才能继续。

这样的同步可以重复执行。前一次已经完成的目标,下一次检查匹配后会跳过;另一个目标临时失败,也能在后续任务中继续补齐。

续签钩子里,为什么要延后核验

这是整套流程里最需要注意的一段。

按当前实现适配的 1Panel 执行顺序,续签后脚本返回之后,面板还会继续提交证书数据、更新站点并完成重载。如果在钩子里同步等待这些步骤完成,就可能一直等不到预期状态。

我把它拆成两个阶段:钩子先完成云端同步及 API 回读,再安排一次独立核验,然后返回;独立核验等待面板完成后续工作,再检查整条链路。

续签钩子先同步云端并返回,1Panel 随后完成提交和重载,独立任务再核验部署结果
续签钩子先同步云端并返回,1Panel 随后完成提交和重载,独立任务再核验部署结果

独立任务通过 systemd-run 安排在约 30 秒后启动,运行结束后自动回收。下面是简化后的调度示意,正式实现还会生成独立任务名并设置超时:

1
2
3
systemd-run --collect --on-active=30s \
/usr/bin/python3 -B /opt/1panel/tools/cert-sync/manage.py \
verify --wait-for-panel --expected-fingerprint '<本次证书的 SHA-256 指纹>'

30 秒只是开始核验的等待时间。进入核验后,对数据库、站点文件、云端配置和 TLS 尚未一致等暂时性问题,程序会间隔 10 秒重试,设置约 300 秒的重试时间窗。网络调用本身也有耗时,因此它并非严格的总墙钟上限。

状态会先记为 verification_pending,等各项检查通过后再改为 success。这时才具备发送成功通知的条件。

独立任务还携带本次证书指纹。如果它启动时发现导出文件已经换成另一张证书,就退出,避免较早的任务对新一轮更新给出过时结论。以后升级 1Panel 时,我也需要重新核对钩子时序,确认这个等待位置仍然合适。

从配置到实际连接,逐层核对

我的成功条件覆盖下面几层:

检查位置检查内容能发现什么
1Panel 数据库证书指纹、有效期、续签开关、导出与钩子配置续签配置被关闭、面板记录未更新
同步导出文件目标指纹、域名覆盖、证书与私钥匹配导出滞后、文件不完整或拿错证书
绑定站点文件各站点证书指纹、私钥匹配和权限某个站点的磁盘证书没有更新
CDN、ESA API当前云端证书、指纹及相关状态云端同步遗漏、仍引用旧证书
线上 TLS实际握手返回的证书、指纹和有效期访问入口仍提供旧证书或连接异常
以目标证书指纹为共同依据,核对面板、导出文件、站点文件、云端配置和线上 TLS
以目标证书指纹为共同依据,核对面板、导出文件、站点文件、云端配置和线上 TLS

线上检查会为域名设置 SNI,使用系统默认的 TLS 校验,再读取握手返回的叶证书计算指纹。指纹用来确认身份一致,TLS 校验负责验证这次连接的证书可信性与域名匹配。

这里也有检查范围:通过代理域名连接时,看到的是该次请求到达的边缘入口。它不能证明每一个边缘节点都已完成部署,也不能直接证明隐藏在代理后面的源站当前提供什么证书。源站磁盘文件检查与外部域名的 TLS 检查,各自提供一部分证据;需要直接验证源站服务时,还应增加指定源站地址、保留正确 SNI 的检查。

2026 年 11 月 5 日,最近一次每日任务的记录中,14 个绑定站点和同步导出目录合计检查了 15 组证书文件,线上 TLS 检查覆盖 17 个域名,结果没有记录异常。这是当时从本机观测到的状态,域名和站点数量变化后,检查范围也会跟着变化。

每日任务负责补偿,文件锁负责协调

续签钩子能及时开始同步,每日任务则提供一个固定的重新检查机会。例如,云端 API 在续签时短暂不可用,或者云端证书后来被手工改回了旧版本,每日运行时都会重新比较并尝试修复差异。

每日检查还会关注自动续签是否开启、导出目录和钩子是否仍在、证书是否临近到期,以及面板申请状态是否异常。当前实现也会报告持续超过两小时的 applying 状态。

这意味着有些故障可能要等到下一次每日任务才会被再次处理。我目前接受这个检查频率;对证书可用性要求更高的服务,可以缩短巡检间隔,或增加独立监控入口。

钩子、每日任务和手动命令还有可能同时执行。脚本使用 flock 串行处理状态文件和云端写入。对续签、每日运行等需要补做的入口,抢锁失败后会安排带目标指纹的延后任务,避免直接丢掉这次工作。过时任务仍由指纹检查拦住。

状态保存在 status.json,更新使用临时文件写入后原子替换。同步进度、最近核验结果和待发送事件可以跨进程保留,下一次运行不需要只凭上一条终端输出猜测结果。

通知跟随事件与核验结果

我通过现有 Hermes 命令行入口发送消息,复用已经配置好的通知平台凭据。证书工具只保存目标和发送入口,不额外复制 Bot Token。

通知大致分为三类:

事件发送条件
更新成功新证书或本次同步完成,并且完整核验通过
异常提醒同步、核验或相关健康检查出现问题
故障恢复之前记录的故障已经通过完整核验

普通每日检查如果没有发现变化或异常,就保持安静。异常通知按证书指纹、故障事件和日期去重;新一轮故障可以独立提醒,持续未解决的问题也可以在后续日期再次提醒。

发送失败的通知先保存在待发送队列中。每次处理前,程序会根据当前证书和健康状态清理过时事件:已经恢复的问题,不应再补发旧故障;已经换过证书,也不应再发送上一张证书的成功消息。

配置多个通知渠道时,按优先级尝试,某个渠道接受后就停止;全部失败才留待后续每日任务重试。这里的“接受”以发送命令成功返回为依据,表示消息已被渠道接收,不能确认人已经阅读。发送结果与证书同步状态也分别记录,方便区分部署故障和通知故障。

留下几个日常维护入口

工具目录里的主要文件如下:

1
2
3
4
5
6
7
8
9
10
/opt/1panel/tools/cert-sync/
├── manage.py # 统一命令入口
├── cdn_cert_sync.py # CDN / ESA 证书比较与同步
├── cert_automation.py # 状态、延迟核验与通知
├── cert_health.py # 本机文件和线上 TLS 检查
├── aliyun_rpc.py # 云端 API 请求
├── dingzh.cc/ # 1Panel 导出的证书与私钥
├── status.json # 最近状态和待发送事件
├── logs/ # 运行摘要
└── backups/ # 云端修改前的配置快照

平时最常用的是三个命令:

1
2
3
4
5
6
7
8
# 查看缓存状态,不联网、不修改文件、不发送通知
python3 -B /opt/1panel/tools/cert-sync/manage.py status

# 重新做只读检查,不同步云端、不发送通知
python3 -B /opt/1panel/tools/cert-sync/manage.py check

# 手动执行一次日常同步与核验,按现有规则处理通知
python3 -B /opt/1panel/tools/cert-sync/manage.py sync --daily

如果需要执行同步但暂时不发消息,可以在第三条命令后加 --no-notify。它仍会更新本地状态,必要时也会更新云端证书。内部的 verify 回调同样会记录状态,并可能发送通知;临时排查时,使用明确只读的 check 更容易判断影响范围。

证书私钥、云端凭据和通知配置限制为 root 访问,私钥权限检查为 600。运行摘要单份限制为 1 MiB,最多保留 3 份;只有实际修改云端配置时才保存修改前快照,并保留最近 5 份。快照不含私钥,因此它能辅助核对旧配置,不能当成完整的证书恢复备份。

卸载也有单独入口,默认只预览:

1
python3 -B /opt/1panel/tools/cert-sync/uninstall.py

确认执行时再加 --yes。卸载会移除本工具登记的调度、钩子调用和工具文件,已有网站证书、云端证书及面板自动续签继续保留,后续云端同步需要另行接管。

这套设计给我的判断依据

现在查看一次证书更新,我可以沿着同一个指纹,从面板记录追到导出文件、站点文件、云端配置,再追到实际 HTTPS 握手。中间有哪一层还没完成,状态和通知就停在对应位置。

这套实现仍然依赖服务器、1Panel 调度和通知渠道正常工作。如果服务器整体离线,本机脚本也无法自己发出告警;后续可以从外部增加独立的证书到期监控,让另一个位置定期检查公开域名。

对目前这些站点,我希望保留的是一条能重复运行、能补做、能解释结果的更新流程。以后新增 CDN 域名、调整 ESA 证书或升级面板时,也有明确的位置去修改和重新验证。

参考资料