wildwindstudio.com 证书自动续签 + 腾讯云 CLB 换绑
本页目录
宿主机:bj-middleware-1(root,TencentOS,腾讯云北京)
2026-09-20 旧证书清理与代理入口验证修复
9 月 22 日到期提醒指向 6 月上传的 YgO1W075 / YgOB9Jx4。9 月 7 日续期和换绑确实成功;原脚本只上传、换绑和验证,没有腾讯云旧证书清理步骤。
另查明 cn-reserve-api 公网入口使用独立 TrustAsia 证书,CLB 源站使用 Let's Encrypt。旧脚本强制比较公网叶证书和源站叶证书,误判部署失败,并在每日 pending 重试时重复上传。
当前部署逻辑由 /usr/local/bin/clb-cert-manager.py 实现,clb-deploy-hook.sh 加载原凭证并调用它:
- 从 CLB API 获取真实源站入口,用业务域名作 SNI,核对源站证书指纹;公网另行验证信任链、域名及有效期,允许入口使用独立证书。
- 已部署目标证书时复用原证书 ID;上传结果持久化,换绑失败重试不重复上传。
- 换绑前持久化旧 ID,新部署验证成功后才能清理旧证书。
- 每日编排末尾独立重试清理队列。必须确认旧证书归属/域名、当前部署健康、全云资源异步查询成功且无绑定,才执行
DeleteCertificate(IsCheckResource=true),等待异步删除成功后才移出队列。查询失败或仍有资源绑定时保留并报错。 - 经用户授权添加官方服务角色
SSL_QCSLinkedRoleInReplaceLoadCertificate,供全资源检查及受保护删除使用。 - 状态:
/var/lib/clb-cert-renewal/<cert-name>.json;备份:/root/cert-renewal-fix-20260920/;日志和每天北京时间 02:00 的调度不变。
三张当前 CLB 证书仍为 abhh3hBR(jsvc)、abhYtjWf(passport)、avq1JprF(cn-reserve-api),源站和公网 TLS 实测通过。10 项测试覆盖资源查询未完成/错误/仍有绑定时禁止删除、异步删除重试、换绑重试复用上传和独立边缘证书验证。
本次全云资源扫描后,已受保护删除 16 张无绑定旧/重复证书(包含 YgO1W075、YgOB9Jx4);最终清单仅剩上述 3 张在用证书。清理队列和部署 pending 均为空,编排入口返回 0,crond 正常。
推荐统一使用 renew-and-update-clb.sh [cert-name ...]。历史应急工具 clb-bind-one.sh 尚不包含新的验证/清理流程,不作为当前推荐入口。
2026-09-07 修复与根因更正
9 月 1 日凌晨自动续期失败,jsvc / passport 线上仍使用 9 月 22 日到期的证书。已确认 certbot-dns-tencentcloud 2.0.2 用 datetime.now() 的本地日期构造 TC3 签名;北京时间 02:00 的日期与 UTC 相差一天。只读 DNSPod API 对照测试复现 AuthFailure.SignatureFailure,日期正确时请求成功。插件又吞掉 API 异常,最终显示 failed to determine base domain。6 月将白天手动成功归因为“错峰避开限流”,证据不足,不能作为已确认根因。
当前脚本:
/etc/crontab每天北京时间 02:00 检查;仅 certbot 子进程设置TZ=UTC,不修改服务器时区。- 默认管理下面映射表的三张证书,也可传入一个或多个 cert-name 定向执行;仍然串行,每张之间间隔 60 秒。
flock防止重复执行;续签或换绑失败返回非零、写日志及本机 syslog,不再统一报告成功。- deploy-hook 校验监听器域名/SNI、等待 CLB 异步任务完成、复查证书 ID,并通过 HTTPS 验证公网叶证书与新签证书一致。
/var/lib/clb-cert-renewal/<cert-name>.pending标记未完成换绑;即使 certbot 返回 0,下次执行也会先重试部署。成功验证后才删除标记。- 备份:服务器
/root/cert-renewal-backup-20260907/。凭据文件未变更。
本次实际续签结果(2026-09-07,脚本退出 0,CLB 与公网 TLS 均已核验):
| 域名 | 旧腾讯云证书 ID | 新腾讯云证书 ID | 新到期时间(北京时间) |
|---|---|---|---|
| jsvc.wildwindstudio.com | YgO1W075 | abhh3hBR | 2026-12-06 10:05:57 |
| passport.wildwindstudio.com | YgOB9Jx4 | abhYtjWf | 2026-12-06 10:04:03 |
cn-reserve-api 本次未手动续签,CLB 仍为 YToxgk9V;它保留在原有自动管理清单,后续每日任务会按续期条件处理。验收时无遗留 pending 标记,crond 正常运行。跨 UTC 日期边界测试、续签失败退出、deploy-hook 失败检测、待部署重试、CLB API 非零退出测试均通过。下次定时检查为北京时间 2026-09-08 02:00。
推荐手动入口(包含 UTC、锁、日志、续签及换绑验证):
以下 6 月排查记录保留作为历史;当前机制以上述修复为准。
1. 整体流程
日志统一写到 /var/log/certbot-renewal.log;certbot 自身调试日志在 /var/log/letsencrypt/。
关键设计点
- 逐张串行 + sleep 60:保留错峰,作为请求节奏控制;它不能修复签名日期错误(见顶部更正)。不要回退成一条
certbot renew批量跑所有证书。 - deploy-hook 模型:只有证书"真的续期"了才会触发上传/换绑,避免无谓地重复上传同一张证书。这点比旧脚本(无条件重传)更正确,但也意味着——一旦续期失败,CLB 就不会被更新,所以续期本身的健康必须被监控(见第 6 节)。
2. 证书 ↔ CLB 映射
region 固定 ap-beijing,当前均为单域名证书:
| 证书(certbot cert-name) | 域名 | CLB | Listener |
|---|---|---|---|
jsvc.wildwindstudio.com-0001 | jsvc.wildwindstudio.com | lb-fkcz6lot | lbl-ehxkll9d |
passport.wildwindstudio.com | passport.wildwindstudio.com | lb-b6j4kn47 | lbl-0jzb58rx |
cn-reserve-api.wildwindstudio.com | cn-reserve-api.wildwindstudio.com | lb-mm70ra69 | lbl-naeanxnv |
映射维护在
clb-deploy-hook.sh的case分支里。新增/变更证书时改这里 +renew-and-update-clb.sh顶部的CERTS=(...)列表。
jsvc证书的 cert-name 带-0001后缀是历史遗留(旧的同名三域名 SAN 谱系占用了jsvc.wildwindstudio.com这个名字),功能无影响。 resflow.wildwindstudio.com 已退役(无 DNS、无在线服务),其旧 CLBlb-mm70ra69现已复用给 cn-reserve-api。
3. 文件清单
| 路径 | 说明 | 权限 |
|---|---|---|
/etc/crontab | 定时入口(已不含明文密钥) | 644 |
/usr/local/bin/renew-and-update-clb.sh | 续签编排脚本 | 755 |
/usr/local/bin/clb-deploy-hook.sh | certbot deploy-hook:上传 + 换绑 CLB | 755 |
/usr/local/bin/clb-bind-one.sh | 手动单张换绑工具(应急用) | 755 |
/etc/letsencrypt/tccli.env | tccli 凭据(上传/换绑用) | 600 |
/etc/letsencrypt/tencentcloud.ini | certbot DNS 插件凭据(DNS-01 写 TXT 用) | 600 |
/var/log/certbot-renewal.log | 续签链路日志 | - |
注意:DNS 插件和 tccli 用的是两套不同的腾讯云 API Key。前者只需 DNSPod 权限,后者需要 SSL + CLB 权限。
4. 排查记录(2026-06-24)
现象
- CLB
lb-fkcz6lot(jsvc.wildwindstudio.com)证书到期,"续期脚本没反应",线上发的是已过期证书。 - 同批的 passport、jsvc 都于 2026-06-24 过期。
排查步骤与发现
- 定位入口:
/etc/crontab里每月 1/15 号 02:00 跑renew-and-update-clb.sh,日志/var/log/certbot-renewal.log。 - 读日志:06-15 那次
certbot renew对 jsvc/resflow 报
即 certbot 续期其实一直在失败。failed to determine base domain, please report to dev. Tried: ['wildwindstudio.com', 'jsvc.wildwindstudio.com'] - 读插件源码
certbot_dns_tencentcloud/certbot_tencentcloud_plugins.py的determine_base_domain():
任何 API 异常(限流 / QPS / 瞬时错误)都被吞掉,最后抛出这条误导性的 "base domain" 错误。= # ← 把任何 API 异常都当成"不拥有这个域名" continue ... - 对照:cn-reserve-api 同账号、同
tencentcloud.ini、同根域,06-16 单独签发就成功了;区别只在"单独跑 vs 批量连跑"。 - staging / 真签复现:白天单张
certbot renew --cert-name jsvc...成功。当时据此推断是限流,但该对照同时改变了运行时间,不能排除签名时区差异;9 月已复现时区缺陷,见顶部更正。 - 旧脚本掩盖了问题:06-16 之前的旧脚本逻辑是"不管 certbot 成不成功,都无条件把现存证书重新上传并换绑 CLB"。所以 04~06 月每次都"✓ 换绑成功",但绑的一直是同一张 03-26 签发、06-24 到期的旧证书,制造了"续期在正常工作"的假象。06-16 改成 deploy-hook 模型后,续期失败就不再换绑,问题才暴露。
当时推断(已更正)
当时怀疑新增域名导致批量请求限流。9 月发现串行错峰仍在凌晨失败,并复现 UTC 日期签名缺陷;不能把 6 月的“单张手动成功”当作限流的证明。
5. 本次处置
应急续期
- 逐张(串行、隔离)手动续签 jsvc / passport,避开限流;用
clb-bind-one.sh上传并换绑对应 CLB。 - 结果:lb-fkcz6lot、lb-b6j4kn47 在线证书均更新,有效期到 2026-09-22。
6 月脚本改造(当时上线;9 月发现未解决时区问题)
renew-and-update-clb.sh改为逐张串行 + 每张 sleep 60 +--no-random-sleep-on-renew,从源头规避限流。clb-deploy-hook.sh修正证书名映射,保留"只在真续期后换绑"。
清理
- 删除过期冗余的 jsvc 三域名 SAN 谱系(含已退役 resflow)。
- 删除含明文密钥的旧
.bak脚本。 - 把明文 tccli 密钥从 644 的
/etc/crontab迁到 600 的/etc/letsencrypt/tccli.env,三脚本统一从此读取;crontab 备份降权 600。
6. 运维 Runbook
手动续签 + 换绑一张证书(应急)
# 1) 续签(务必单张、隔离跑,别一次跑多张)
TZ=UTC
# 2) 上传 + 换绑(凭据自动从 /etc/letsencrypt/tccli.env 读)
验证 CLB 在线证书
| |
查看证书清单 / 到期
新增一个域名/证书
certbot certonly --dns-tencentcloud --dns-tencentcloud-credentials /etc/letsencrypt/tencentcloud.ini -d <域名>- 在
clb-deploy-hook.sh的case加映射;在renew-and-update-clb.sh的CERTS=(...)加 cert-name。 - 首次手动
clb-bind-one.sh绑一次。
待办 / 改进项
-
轮换 tccli API Key(
AKIDqgLrCLI2...):曾在 644 crontab 里明文存放数月,按泄漏处理。 - 加证书剩余天数告警(接现有 Prometheus + 钉钉),剩 < 14 天即告警;别再依赖"脚本没报错"。
-
可考虑给
certbot-dns-tencentcloud插件加重试/退避(注意:改 site-packages 升级会被覆盖;串行错峰本身不解决时区或其他 API 错误)。