本页目录

wildwindstudio.com 证书自动续签 + 腾讯云 CLB 换绑

宿主机:bj-middleware-1(root,TencentOS,腾讯云北京)


1. 整体流程

整体流程:定时触发 → 逐张续签 → 仅成功时换绑 CLB /etc/crontab · 每月 1、15 号 02:00 触发 renew-and-update-clb.sh · 编排脚本(核心) 对每张证书执行 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 监听器更新为新证书 关键设计:逐张串行 + sleep 60 从源头避开腾讯云 DNS 接口限流; deploy-hook 只在真续期成功后才换绑 —— 失败不会误更新,但也意味着续期本身必须被监控。

日志统一写到 /var/log/certbot-renewal.log;certbot 自身调试日志在 /var/log/letsencrypt/

关键设计点

  • 逐张串行 + sleep 60:根本目的是避开腾讯云 DNS 接口限流(见第 4 节根因)。⁠不要回退成一条 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.shcase 分支里。新增/变更证书时改这里 + 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.pydetermine_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... 直接越过 base-domain 这步、续期成功 → 实锤是批量连跑触发限流⁠,被插件吞成了 base-domain 错误。
  6. 旧脚本掩盖了问题⁠:06-16 之前的旧脚本逻辑是"不管 certbot 成不成功,都无条件把现存证书重新上传并换绑 CLB"。所以 04~06 月每次都"✓ 换绑成功",但绑的一直是同一张 03-26 签发、06-24 到期的旧证书⁠,制造了"续期在正常工作"的假象。06-16 改成 deploy-hook 模型后,续期失败就不再换绑,问题才暴露。

根因

预约(cn-reserve)服务新增 2 个域名后,单次续签批要处理的域名增多,短时间内密集调用腾讯云 DNS 接口 → 触发限流 → certbot DNS 校验拿不到结果 → 续期失败 → 证书到期未能自动更新。


5. 本次处置

应急续期

  • 逐张(串行、隔离)手动续签 jsvc / passport,避开限流;用 clb-bind-one.sh 上传并换绑对应 CLB。
  • 结果:lb-fkcz6lot、lb-b6j4kn47 在线证书均更新,有效期到 2026-09-22

治本(脚本改造,已上线 + 自测 EXIT=0)

  • 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
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.shcase 加映射;在 renew-and-update-clb.shCERTS=(...) 加 cert-name。
  3. 首次手动 clb-bind-one.sh 绑一次。

待办 / 改进项

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