CloudflareDNS和腾讯云CDN自动续期免费HTTPS证书
www.liuvv.com 的证书以前经常过期。表面看像是「域名在 Cloudflare,证书却没续上」,但真实结构并非如此:Cloudflare 只负责 DNS,访客真正访问的是腾讯云 CDN,浏览器看到的证书也安装在腾讯云 CDN 上。
本文记录一套适配该场景的免费自动化方案:GitHub Actions 定时运行 acme.sh,通过 Cloudflare DNS-01 验证申请 Let’s Encrypt 证书,再调用腾讯云 SSL/CDN API,把证书部署到 www.liuvv.com 的 CDN 加速域名。
1. 先分清两个链路
证书自动化最容易混乱的地方,是把「访问链路」和「续期链路」混在一起。先看访问链路。
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart LR
A["浏览器 HTTPS 访问"] --> B["Cloudflare DNS 查询"]
B --> C["返回 CNAME 记录"]
C --> D["腾讯云 CDN 边缘节点"]
D --> E["腾讯云 COS Website 源站"]
D --> F["返回 HTTPS 证书和网页内容"]
F --> A
classDef dns fill:#DBEAFE,stroke:#2563EB,color:#1E3A5F;
classDef cdn fill:#FEF3C7,stroke:#F59E0B,color:#78350F;
classDef user fill:#DCFCE7,stroke:#10B981,color:#064E3B;
class B,C dns;
class D,E,F cdn;
class A user;具体域名是 www.liuvv.com,CNAME 指向 www.liuvv.com.cdn.dnsv1.com。
这条链路里,Cloudflare 的角色是权威 DNS。它告诉浏览器 www.liuvv.com 应该去找腾讯云 CDN,但不承载该域名的流量,也不终止浏览器侧 TLS。
因此证书过期时,不要说「Cloudflare 证书过期」。对这个站点,更准确的说法是:
1 | 腾讯云 CDN 边缘证书过期了。 |
分清访问链路后,下一步要回答:为什么不用腾讯云控制台里现成的免费证书?
2. 为什么不用腾讯云免费证书控制台?
如果 DNS、CDN、证书都在腾讯云,控制台里的免费证书申请、自动续费、证书托管可能是最简单的路。
但 www.liuvv.com 的结构并非如此:
1 | DNS 托管:Cloudflare |
目标也不是「拿到一张新证书」这么简单,而是:
1 | 签发证书 |
如果只完成第一步,网站仍可能继续使用旧证书,或者证书有效但 HTTPS 服务被关掉。腾讯云 CDN 在 HTTPS 服务关闭时会直接返回 514。
因此这里选择 API 自动化:ACME 申请免费证书,Cloudflare 做 DNS-01 验证,腾讯云 API 完成上传和部署。下面先看完整续期链路。
3. 自动续期方案总览
完整续期链路如下:
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart TD
A["GitHub Actions 定时运行"] --> B["acme.sh 开始申请证书"]
B --> C["Let's Encrypt 要求验证域名控制权"]
C --> D["acme.sh 调用 Cloudflare API"]
D --> E["Cloudflare 添加 _acme-challenge TXT 记录"]
E --> F["Let's Encrypt 查询 DNS TXT 记录"]
F --> G["签发免费证书"]
G --> H["GitHub Actions 读取证书和私钥"]
H --> I["腾讯云 UploadCertificate 上传证书"]
I --> J["腾讯云 DeployCertificateInstance 部署到 CDN"]
J --> K["公网检查 TLS 证书"]
K --> L["公网检查首页返回 2xx 或 3xx"]
classDef dns fill:#DBEAFE,stroke:#2563EB,color:#1E3A5F;
classDef cert fill:#DCFCE7,stroke:#10B981,color:#064E3B;
classDef cdn fill:#FEF3C7,stroke:#F59E0B,color:#78350F;
classDef verify fill:#FEE2E2,stroke:#EF4444,color:#7F1D1D;
class D,E,F dns;
class B,C,G,H cert;
class I,J cdn;
class K,L verify;Cloudflare 出现在续期链路里,是因为 Let’s Encrypt 需要确认我控制 www.liuvv.com。DNS-01 验证会临时添加一条 TXT 记录:
1 | _acme-challenge.www.liuvv.com TXT <challenge-token> |
谁能添加这条记录,谁就能证明自己控制这个域名。liuvv.com 的 DNS 在 Cloudflare,acme.sh 必须拿 Cloudflare API Token 去添加和删除这条 TXT 记录。
链路清楚了,接下来分别准备 Cloudflare 和腾讯云的凭据。
4. 准备 Cloudflare Token
Cloudflare 只需要给 ACME 验证用,不需要让它接管访客流量。
在 Cloudflare Dashboard 里创建 API Token:
- 进入右上角头像菜单,打开
Profile。 - 进入
API Tokens。 - 点击
Create Token。 - 使用
Edit zone DNS模板,或自定义 token。 - 权限只给
Zone > DNS > Edit。 - Zone 资源只选择
liuvv.com。
最后得到两个值:
1 | CLOUDFLARE_API_TOKEN=*** |
CLOUDFLARE_API_TOKEN 是密钥,不要写进仓库,也不要发到聊天记录里。CLOUDFLARE_ZONE_ID 不是私钥,也建议统一放到 GitHub Actions Secrets,避免到处散落。
Cloudflare 凭据就绪后,再创建腾讯云 CAM 子用户。
5. 准备腾讯云 CAM 子用户密钥
腾讯云这边需要调用两个能力:
1 | UploadCertificate:上传证书到腾讯云 SSL |
推荐创建一个单独的 CAM 子用户,例如:
1 | github-actions-cdn-cert |
访问方式只开「编程访问」,不需要控制台登录。策略先按最小权限写:
1 | { |
然后给这个子用户创建 API 密钥,得到:
1 | TENCENT_SECRET_ID=*** |
这里有一个安全边界要说清楚:主账号密钥也能跑通,但不适合作为长期方案。主账号密钥权限太大,一旦泄露很难收敛影响。正式配置应该用 CAM 子用户,按实际报错逐步补权限,不要一开始就给全局管理员权限。
腾讯云的 SecretKey 创建后通常不能再次查看。创建时就要保存到密码管理器;如果丢了,只能新建一组密钥并更新 GitHub Secrets。
两边凭据都准备好之后,把它们写入 GitHub 仓库配置。
6. 配置 GitHub Secrets 和 Variables
仓库里需要 5 个 Secrets:
| 名称 | 用途 |
|---|---|
ACME_EMAIL | 注册 Let’s Encrypt 账户 |
CLOUDFLARE_API_TOKEN | 添加和删除 DNS-01 TXT 记录 |
CLOUDFLARE_ZONE_ID | 限定 Cloudflare zone |
TENCENT_SECRET_ID | 腾讯云 API SecretId |
TENCENT_SECRET_KEY | 腾讯云 API SecretKey |
用 GitHub CLI 可以这样写入:
1 | gh secret set ACME_EMAIL --repo unix2dos/blog-liuvv |
命令会等待你输入值,输入内容不会回显。
还需要一个 GitHub Variable:
1 | gh variable set TENCENT_CDN_BILLING --repo unix2dos/blog-liuvv --body on |
这个值看起来像「计费」,但它实际影响腾讯云 CDN HTTPS 服务开关。对已经使用 HTTPS 的站点,要保持为 on。如果传 off,证书可能部署成功,但 HTTPS 请求会被腾讯云 CDN 拒绝,返回 514。
Secrets 配好后,就可以写 GitHub Actions 工作流了。
7. GitHub Actions 工作流怎么写?
工作流放在:
1 | .github/workflows/tencent-cdn-certificate.yml |
核心结构如下:
1 | name: Renew Tencent CDN certificate |
这里有两个入口:
- 每月 1 日自动跑一次。
- 手动触发,可以选择
dry_run。
dry_run 只检查部署 payload,不申请证书,也不调用云 API。第一次配置时建议先跑它。
真正签发证书的核心步骤是:
1 | - name: Issue certificate |
CF_Token 和 CF_Zone_ID 是 acme.sh 的 Cloudflare DNS 插件会读取的环境变量。--dns dns_cf 表示用 Cloudflare DNS API 完成 DNS-01 验证。
证书签发成功后,还要上传并部署到腾讯云 CDN。
8. 上传并部署到腾讯云 CDN
证书签发后,acme.sh 会在工作目录里留下两份关键文件:
1 | .acme.sh/www.liuvv.com/fullchain.cer |
然后调用本仓库脚本:
1 | - name: Deploy certificate to Tencent CDN |
脚本做两件事。
第一步,调用腾讯云 SSL 的 UploadCertificate:
1 | { |
第二步,调用 DeployCertificateInstance:
1 | { |
重点是 www.liuvv.com|on。腾讯云文档里,CDN 资源的 InstanceIdList 形态是:
1 | Domain|计费开关 |
所以这里不是随便拼了一个字符串。on 表示部署证书时保持 CDN HTTPS 服务开启。
部署完成后,还需要从公网侧验证结果——而且不能只看证书。
9. 验证不能只看证书
最开始我只验证了 TLS 证书:
1 | docker-compose run --rm hexo-liuvv node tools/check-domain-certificate.js \ |
这个检查能证明:
- 浏览器能拿到证书。
- 证书域名覆盖
www.liuvv.com。 - 签发方是 Let’s Encrypt。
- 剩余有效期不少于 60 天。
但它不能证明首页真的能打开。HTTPS 握手成功后,CDN 仍可能返回 514、403、5xx。
所以工作流还要加一个公网首页检查:
1 | - name: Verify public CDN HTTP access |
证书检查和首页检查要一起看:
1 | TLS 证书有效:只能说明证书部署上去了 |
验证策略定下来之后,下面是一个真实踩过的坑。
10. 常见坑:证书有效,但返回 514
这次真实踩到的坑是:
1 | 证书已经是新的 Let's Encrypt |
排查时先看三件事:
1 | dig +short www.liuvv.com CNAME |
当时看到的结果是:
1 | DNS: www.liuvv.com -> www.liuvv.com.cdn.dnsv1.com |
这说明 Cloudflare DNS 没坏,证书也不是主要问题。真正的问题在腾讯云 CDN 侧。
腾讯云 CDN 的 514 常见原因包括:
- 没有启用 HTTPS 服务,却用 HTTPS 访问。
- 命中 IP 黑白名单。
- 命中 IP 访问限频。
本次原因是第一种:部署证书时把 TENCENT_CDN_BILLING 设成了 off,相当于关闭了 CDN HTTPS 服务。⚠️ 易错点:InstanceIdList 里的 off 会关掉 CDN HTTPS 服务,证书再新也会返回 514。
修复方式是:
1 | gh variable set TENCENT_CDN_BILLING --repo unix2dos/blog-liuvv --body on |
然后重新触发一次真实部署。恢复后,公网检查应该类似:
1 | HTTP/2 200 |
方案和排障都讲完了,完整代码和本地检查脚本见仓库文件。
11. 本地脚本和完整文件
正文只放关键片段,完整实现放在仓库文件里:
1 | .github/workflows/tencent-cdn-certificate.yml |
本地契约检查:
1 | docker-compose run --rm hexo-liuvv node tools/check-tencent-cdn-cert-deploy.js |
注意本仓库的 Node/Hexo 命令都通过 docker-compose 执行,不直接使用宿主机 Node 环境。
本地脚本可用于改工作流前的回归验证。跑通之后,长期维护重点如下。
12. 后续维护
这套方案跑通后,长期维护重点有四个。
第一,定时任务不要太频繁。Let’s Encrypt 有频率限制,每月跑一次足够覆盖 90 天证书周期。
第二,密钥只放 GitHub Secrets,不进仓库。Cloudflare Token 和腾讯云 SecretKey 都不要写进 Markdown、YAML、脚本默认值或聊天记录。
第三,腾讯云主账号密钥只适合临时验证。验证通过后,应该迁移到 CAM 子用户,并禁用或删除主账号密钥。
第四,监控目标要看「网站可访问」,不要只看「证书未过期」。这次 514 已经证明,证书有效但网站仍然可能打不开。
各组件职责可以记成一张对照表:
1 | Cloudflare:DNS 托管,给 DNS-01 验证用 |
自动续期不是拿到证书就结束,而是要让腾讯云 CDN 用上新证书,并确认 https://www.liuvv.com/ 返回 2xx 或 3xx。