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.comYgO1W075abhh3hBR2026-12-06 10:05:57
passport.wildwindstudio.comYgOB9Jx4abhYtjWf2026-12-06 10:04:03

cn-reserve-api 本次未手动续签,CLB 仍为 YToxgk9V;它保留在原有自动管理清单,后续每日任务会按续期条件处理。验收时无遗留 pending 标记,crond 正常运行。跨 UTC 日期边界测试、续签失败退出、deploy-hook 失败检测、待部署重试、CLB API 非零退出测试均通过。下次定时检查为北京时间 2026-09-08 02:00。

推荐手动入口(包含 UTC、锁、日志、续签及换绑验证):

/usr/local/bin/renew-and-update-clb.sh passport.wildwindstudio.com jsvc.wildwindstudio.com-0001

以下 6 月排查记录保留作为历史;当前机制以上述修复为准。


1. 整体流程

整体流程:定时触发 → 逐张续签 → 仅成功时换绑 CLB /etc/crontab · 每天北京时间 02:00 触发 renew-and-update-clb.sh · 编排脚本(核心) 对每张证书执行 TZ=UTC certbot renew --no-random-sleep-on-renew --dns-tencentcloud-propagation-seconds 30 --deploy-hook …;每张之间 sleep 60(保留错峰) certbot(Let's Encrypt · DNS-01 校验) 用 certbot-dns-tencentcloud 插件在 DNSPod 写 _acme-challenge TXT · 凭据 tencentcloud.ini 仅当某张证书"真的续期成功"时才调用 → clb-deploy-hook.sh(deploy-hook,仅续期成功时被调用) ① tccli ssl UploadCertificate — 上传新证书到腾讯云 SSL,得到 CertId ② tccli clb ModifyListener — 把 CertId 换绑到对应 CLB listener · 凭据 tccli.env(600) 腾讯云 CLB 监听器更新为新证书 关键设计:TZ=UTC 修复签名日期;逐张串行 + sleep 60 保留错峰; deploy-hook 只在真续期成功后才换绑 —— 失败不会误更新,但也意味着续期本身必须被监控。

日志统一写到 /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)域名CLBListener
jsvc.wildwindstudio.com-0001jsvc.wildwindstudio.comlb-fkcz6lotlbl-ehxkll9d
passport.wildwindstudio.compassport.wildwindstudio.comlb-b6j4kn47lbl-0jzb58rx
cn-reserve-api.wildwindstudio.comcn-reserve-api.wildwindstudio.comlb-mm70ra69lbl-naeanxnv

映射维护在 clb-deploy-hook.sh 的 case 分支里。新增/变更证书时改这里 + renew-and-update-clb.sh 顶部的 CERTS=(...) 列表。

jsvc 证书的 cert-name 带 -0001 后缀是历史遗留(旧的同名三域名 SAN 谱系占用了 jsvc.wildwindstudio.com 这个名字),功能无影响。 resflow.wildwindstudio.com 已退役⁠(无 DNS、无在线服务),其旧 CLB lb-mm70ra69 现已复用给 cn-reserve-api。


3. 文件清单

路径说明权限
/etc/crontab定时入口(已不含明文密钥)644
/usr/local/bin/renew-and-update-clb.sh续签编排脚本755
/usr/local/bin/clb-deploy-hook.shcertbot deploy-hook:上传 + 换绑 CLB755
/usr/local/bin/clb-bind-one.sh手动单张换绑工具(应急用)755
/etc/letsencrypt/tccli.envtccli 凭据(上传/换绑用)600
/etc/letsencrypt/tencentcloud.inicertbot 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 过期。

排查步骤与发现

  1. 定位入口⁠:/etc/crontab 里每月 1/15 号 02:00 跑 renew-and-update-clb.sh,日志 /var/log/certbot-renewal.log。
  2. 读日志⁠:06-15 那次 certbot renew 对 jsvc/resflow 报
    failed to determine base domain, please report to dev.
    Tried: ['wildwindstudio.com', 'jsvc.wildwindstudio.com']
    
    即 certbot 续期其实一直在失败⁠。
  3. 读插件源码 certbot_dns_tencentcloud/certbot_tencentcloud_plugins.py 的 determine_base_domain():
    try:
        resp = client.describe_record_list(dt)
    except APIException as _:   # ← 把任何 API 异常都当成"不拥有这个域名"
        continue
    ...
    raise PluginError("failed to determine base domain ...")
    
    任何 API 异常(限流 / QPS / 瞬时错误)都被吞掉⁠,最后抛出这条误导性的 "base domain" 错误。
  4. 对照⁠:cn-reserve-api 同账号、同 tencentcloud.ini、同根域,06-16 单独签发就成功了;区别只在"单独跑 vs 批量连跑"。
  5. staging / 真签复现⁠:白天单张 certbot renew --cert-name jsvc... 成功。当时据此推断是限流,但该对照同时改变了运行时间,不能排除签名时区差异;9 月已复现时区缺陷,见顶部更正。
  6. 旧脚本掩盖了问题⁠: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) 续签(务必单张、隔离跑,别一次跑多张)
export PATH=/usr/local/bin:$PATH
TZ=UTC certbot renew --cert-name <cert-name> \
  --no-random-sleep-on-renew \
  --dns-tencentcloud-propagation-seconds 30

# 2) 上传 + 换绑(凭据自动从 /etc/letsencrypt/tccli.env 读)
/usr/local/bin/clb-bind-one.sh \
  /etc/letsencrypt/live/<cert-name> <alias> <lb-id> <listener-id>

验证 CLB 在线证书

echo | openssl s_client -connect <域名>:443 -servername <域名> 2>/dev/null \
  | openssl x509 -noout -subject -dates

查看证书清单 / 到期

certbot certificates

新增一个域名/证书

  1. certbot certonly --dns-tencentcloud --dns-tencentcloud-credentials /etc/letsencrypt/tencentcloud.ini -d <域名>
  2. 在 clb-deploy-hook.sh 的 case 加映射;在 renew-and-update-clb.sh 的 CERTS=(...) 加 cert-name。
  3. 首次手动 clb-bind-one.sh 绑一次。

待办 / 改进项

  • 轮换 tccli API Key(AKIDqgLrCLI2...):曾在 644 crontab 里明文存放数月,按泄漏处理。
  • 加证书剩余天数告警⁠(接现有 Prometheus + 钉钉),剩 < 14 天即告警;别再依赖"脚本没报错"。
  • 可考虑给 certbot-dns-tencentcloud 插件加重试/退避(注意:改 site-packages 升级会被覆盖;串行错峰本身不解决时区或其他 API 错误)。