---
title: wildwindstudio.com 证书自动续签 + 腾讯云 CLB 换绑
url: https://doc.liz6.com/devops/runbooks/wildwind-clb-cert-renewal
locale: zh
area: devops
tags:
- devops
- runbooks
date: 2026-06-30
modified: 2026-07-11
description: 宿主机：bj-middleware-1（root，TencentOS，腾讯云北京）
---

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

> 宿主机：**bj-middleware-1**（root，TencentOS，腾讯云北京）

---

## 1. 整体流程

<svg viewBox="0 0 720 420" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="证书自动续签整体流程:从定时触发到腾讯云CLB换绑">
  <defs>
    <marker id="wcah1" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="420" fill="#ffffff"/>
  <text x="360" y="24" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">整体流程:定时触发 → 逐张续签 → 仅成功时换绑 CLB</text>

  <rect x="110" y="40" width="500" height="32" rx="6" fill="#e2e8f0"/>
  <text x="360" y="60" text-anchor="middle" font-size="12" fill="#334155">/etc/crontab · 每月 1、15 号 02:00 触发</text>

  <line x1="360" y1="72" x2="360" y2="83" stroke="#475569" stroke-width="1.6" marker-end="url(#wcah1)"/>

  <rect x="60" y="84" width="600" height="62" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="104" font-size="12.5" font-weight="700" fill="#3730a3">renew-and-update-clb.sh · 编排脚本(核心)</text>
  <text x="76" y="121" font-size="10.5" fill="#4f46e5">对每张证书执行 certbot renew --no-random-sleep-on-renew</text>
  <text x="76" y="137" font-size="10.5" fill="#4f46e5">--dns-tencentcloud-propagation-seconds 30 --deploy-hook …;每张完成后 sleep 60(错峰,避开限流)</text>

  <line x1="360" y1="146" x2="360" y2="159" stroke="#475569" stroke-width="1.6" marker-end="url(#wcah1)"/>

  <rect x="60" y="160" width="600" height="48" rx="8" fill="#e0e7ff"/>
  <text x="76" y="178" font-size="12" font-weight="700" fill="#3730a3">certbot(Let's Encrypt · DNS-01 校验)</text>
  <text x="76" y="196" font-size="10.5" fill="#4338ca">用 certbot-dns-tencentcloud 插件在 DNSPod 写 _acme-challenge TXT · 凭据 tencentcloud.ini</text>

  <line x1="360" y1="208" x2="360" y2="221" stroke="#475569" stroke-width="1.6" marker-end="url(#wcah1)"/>
  <text x="380" y="218" font-size="10.5" font-weight="600" fill="#c2410c">仅当某张证书"真的续期成功"时才调用 →</text>

  <rect x="60" y="222" width="600" height="62" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="76" y="242" font-size="12.5" font-weight="700" fill="#115e59">clb-deploy-hook.sh(deploy-hook,仅续期成功时被调用)</text>
  <text x="76" y="260" font-size="10.5" fill="#0f766e">① tccli ssl UploadCertificate — 上传新证书到腾讯云 SSL,得到 CertId</text>
  <text x="76" y="276" font-size="10.5" fill="#0f766e">② tccli clb ModifyListener — 把 CertId 换绑到对应 CLB listener · 凭据 tccli.env(600)</text>

  <line x1="360" y1="284" x2="360" y2="297" stroke="#475569" stroke-width="1.6" marker-end="url(#wcah1)"/>

  <rect x="110" y="298" width="500" height="34" rx="6" fill="#dcfce7" stroke="#4ade80"/>
  <text x="360" y="320" text-anchor="middle" font-size="12" font-weight="700" fill="#166534">腾讯云 CLB 监听器更新为新证书</text>

  <rect x="60" y="352" width="600" height="54" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="374" font-size="12.5" fill="#3730a3">关键设计:逐张串行 + sleep 60 从源头避开腾讯云 DNS 接口限流;</text>
  <text x="76" y="392" font-size="12.5" fill="#3730a3">deploy-hook 只在真续期成功后才换绑 —— 失败不会误更新,但也意味着续期本身必须被监控。</text>
</svg>

日志统一写到 `/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）            | 域名                              | 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、无在线服务），其旧 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.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 过期。

### 排查步骤与发现
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()`：
   ```python
   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

### 手动续签 + 换绑一张证书（应急）
```bash
# 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 在线证书
```bash
echo | openssl s_client -connect <域名>:443 -servername <域名> 2>/dev/null \
  | openssl x509 -noout -subject -dates
```

### 查看证书清单 / 到期
```bash
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 升级会被覆盖；当前靠脚本层串行+错峰已足够）。
