ClouDNS 免费域名托管到 Cloudflare 后证书申请失败的自动化解决方案
ClouDNS 免费域名托管到 Cloudflare 后证书申请失败的自动化解决方案
当你把 ClouDNS 的免费域名(如
ddns-ip.net子域名)添加到 Cloudflare 时,由于免费用户无法修改 SOA 和 NS 记录,Cloudflare 的 SSL 证书验证会失败。本文介绍如何利用两边的 API 实现证书自动签发。
背景
ClouDNS 提供免费的 DNS 托管服务,支持 ddns-ip.net、cloudns.nz 等免费子域名。许多用户希望将这些域名接入 Cloudflare 以获得 CDN、DDoS 防护和自动 SSL 证书。
然而,Cloudflare 在签发 Edge Certificate 时,会通过 ACME 协议向域名的权威 DNS 服务器查询 _acme-challenge TXT 记录来完成域名所有权验证。由于 ClouDNS 免费账户的限制,问题随之而来。
问题分析
核心矛盾
| 项目 | 现状 | 影响 |
|---|---|---|
| SOA/NS 记录 | 免费用户无法修改,始终指向 ClouDNS | Cloudflare 无法成为权威 DNS |
| 证书验证方式 | TXT 记录验证(DNS-01) | 证书颁发机构会去 ClouDNS 的 NS 查询 |
| TXT 记录位置 | 必须在 ClouDNS 添加 | 在 Cloudflare 面板添加无效 |
简单来说:Cloudflare 告诉你需要什么 TXT 记录,但你必须去 ClouDNS 那边添加,因为权威 NS 在 ClouDNS 手上。
证书验证信息示例
在 Cloudflare 控制面板的 SSL/TLS → Edge Certificates 中,你会看到类似这样的验证信息:
证书验证 TXT 名称: _acme-challenge.*******.ddns-ip.net
证书验证 TXT 值: lmBBknDTmOKc2FPbLAzgamZqclHz7h_IljWTCRQ-81c
证书验证 TXT 名称: _acme-challenge.*************************.*******.ddns-ip.net
证书验证 TXT 值: U0U-g2ZMKja7iRBMAT4IhfDP9ZO7pfBOiJ7wTVRLVAs
证书验证 TXT 值: l8xfO5L5UTNPJAK7eOAsG8zjJDBCoW5_u-x13YOo2zg
注意同一个 host 下可能有多个 TXT 值,都需要添加。
手动解决方案
如果只是偶尔需要,可以手动操作:
- 登录 ClouDNS 控制面板
- 进入域名的 DNS 区域
- 逐条添加 TXT 记录
| Host | 类型 | 值 |
|---|---|---|
_acme-challenge |
TXT | lmBBknDTmOKc2FPbLAzgamZqclHz7h_IljWTCRQ-81c |
_acme-challenge.************************* |
TXT | U0U-g2ZMKja7iRBMAT4IhfDP9ZO7pfBOiJ7wTVRLVAs |
_acme-challenge.************************* |
TXT | l8xfO5L5UTNPJAK7eOAsG8zjJDBCoW5_u-x13YOo2zg |
- 等待 DNS 传播,用
dig验证:
dig @ns1.cloudns.net TXT _acme-challenge.*******.ddns-ip.net
dig @ns1.cloudns.net TXT _acme-challenge.*************************.*******.ddns-ip.net
- 回到 Cloudflare 控制面板检查证书状态
问题:证书每 3 个月续期一次,每次续期都会生成新的验证值,手动操作不可持续。
自动化解决方案
整体架构
┌─────────────┐ 1. 查询证书状态 ┌──────────────┐
│ 定时脚本 │ ────────────────────→ │ Cloudflare │
│ (Cron Job) │ ←──────────────────── │ API │
│ │ 2. 返回TXT验证信息 │ │
│ │ └──────────────┘
│ │
│ │ 3. 添加/更新TXT记录 ┌──────────────┐
│ │ ────────────────────→ │ ClouDNS │
│ │ ←──────────────────── │ API │
│ │ 4. 确认操作成功 │ │
└─────────────┘ └──────────────┘
│
│ 5. 等待DNS传播 + Cloudflare自动验证
▼
证书签发成功
前置准备
Cloudflare 侧
- 创建 API Token:Cloudflare 控制面板 → My Profile → API Tokens → Create Token
- 所需权限:
Zone:SSL and Certificates:Read、Zone:SSL and Certificates:Edit - 记录下
Zone ID(在域名概览页右下角)
ClouDNS 侧
- 登录 ClouDNS 控制面板 → Settings → API
- 启用 API 访问,获取
auth-id和auth-password - 免费用户也可使用 API 管理 TXT 记录
核心 API 端点
Cloudflare API
# 获取 Zone ID(如果不知道的话)
GET https://api.cloudflare.com/client/v4/zones?name=*******.ddns-ip.net
# 查询证书验证状态 —— 核心!获取需要添加的 TXT 记录信息
GET https://api.cloudflare.com/client/v4/zones/{zone_id}/ssl/verification
# 触发重新验证(添加完 TXT 记录后调用,加速验证流程)
PATCH https://api.cloudflare.com/client/v4/zones/{zone_id}/ssl/verification
ssl/verification 返回的关键字段:
{
"result": [{
"certificate_status": "pending_validation",
"verification_type": "txt",
"verification_info": [{
"record_name": "_acme-challenge.*******.ddns-ip.net",
"record_value": "lmBBknDTmOKc2FPbLAzgamZqclHz7h_IljWTCRQ-81c"
}],
"cert_pack_uuid": "...",
"hosts": [
"*******.ddns-ip.net",
"*.*************************.*******.ddns-ip.net",
"*************************.*******.ddns-ip.net"
]
}]
}
ClouDNS API
# 列出域名下的所有记录(用于查找和比对已有 TXT 记录)
GET https://api.cloudns.net/dns/records.json
?auth-id={id}
&auth-password={password}
&domain-name=*******.ddns-ip.net
# 添加 TXT 记录
POST https://api.cloudns.net/dns/add-record.json
auth-id={id}
auth-password={password}
domain-name=*******.ddns-ip.net
record-type=TXT
host=_acme-challenge
record=lmBBknDTmOKc2FPbLAzgamZqclHz7h_IljWTCRQ-81c
ttl=60
# 修改已有记录(更新验证值)
POST https://api.cloudns.net/dns/mod-record.json
auth-id={id}
auth-password={password}
domain-name=*******.ddns-ip.net
record-id={id}
record=新的验证值
# 删除记录
POST https://api.cloudns.net/dns/delete-record.json
auth-id={id}
auth-password={password}
domain-name=*******.ddns-ip.net
record-id={id}
完整自动化脚本
#!/usr/bin/env python3
"""
Cloudflare + ClouDNS 证书自动验证脚本
功能:定时检查 Cloudflare 证书状态,自动将 ACME 验证 TXT 记录同步到 ClouDNS。
适用于 ClouDNS 免费域名无法修改 NS/SOA 记录的场景。
依赖:requests (pip install requests)
"""
import requests
import time
import os
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)
# ===================== 配置 =====================
CF_API_TOKEN = os.environ.get("CF_API_TOKEN", "")
CF_ZONE_ID = os.environ.get("CF_ZONE_ID", "")
CLOUDNS_AUTH_ID = os.environ.get("CLOUDNS_AUTH_ID", "")
CLOUDNS_AUTH_PASSWORD = os.environ.get("CLOUDNS_AUTH_PASSWORD", "")
DOMAIN = os.environ.get("DOMAIN", "*******.ddns-ip.net")
DNS_PROPAGATION_WAIT = 60 # 添加记录后等待传播的秒数
# ================================================
CF_API = "https://api.cloudflare.com/client/v4"
CF_HEADERS = {"Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json"}
CLOUDNS_AUTH = {"auth-id": CLOUDNS_AUTH_ID, "auth-password": CLOUDNS_AUTH_PASSWORD}
def cf_get_verification():
"""查询 Cloudflare 证书验证状态"""
resp = requests.get(
f"{CF_API}/zones/{CF_ZONE_ID}/ssl/verification",
headers=CF_HEADERS,
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
log.error("Cloudflare API error: %s", data.get("errors"))
return []
return data["result"]
def cf_trigger_verification():
"""触发 Cloudflare 重新验证证书"""
resp = requests.patch(
f"{CF_API}/zones/{CF_ZONE_ID}/ssl/verification",
headers=CF_HEADERS,
)
resp.raise_for_status()
data = resp.json()
if data.get("success"):
log.info("已触发 Cloudflare 重新验证")
else:
log.warning("触发验证失败: %s", data.get("errors"))
def cloudns_list_records():
"""列出 ClouDNS 域名下所有记录"""
params = {**CLOUDNS_AUTH, "domain-name": DOMAIN}
resp = requests.get("https://api.cloudns.net/dns/records.json", params=params)
resp.raise_for_status()
return resp.json()
def cloudns_add_txt(host, value, ttl=60):
"""在 ClouDNS 添加 TXT 记录"""
params = {
**CLOUDNS_AUTH,
"domain-name": DOMAIN,
"record-type": "TXT",
"host": host,
"record": value,
"ttl": ttl,
}
resp = requests.post("https://api.cloudns.net/dns/add-record.json", params=params)
resp.raise_for_status()
data = resp.json()
if data.get("status") == "Success":
log.info("添加 TXT 记录成功: %s -> %s", host, value[:20] + "...")
else:
log.error("添加 TXT 记录失败: %s", data)
def cloudns_delete_record(record_id):
"""在 ClouDNS 删除记录"""
params = {**CLOUDNS_AUTH, "domain-name": DOMAIN, "record-id": record_id}
resp = requests.post("https://api.cloudns.net/dns/delete-record.json", params=params)
resp.raise_for_status()
log.info("删除记录: id=%s", record_id)
def extract_host(record_name):
"""
将完整记录名转为 ClouDNS 的 host 字段
例: _acme-challenge.*******.ddns-ip.net -> _acme-challenge
_acme-challenge.sub.*******.ddns-ip.net -> _acme-challenge.sub
"""
suffix = f".{DOMAIN}"
if record_name.endswith(suffix):
return record_name[: -len(suffix)]
return record_name
def sync_txt_records():
"""主逻辑:同步 Cloudflare 验证 TXT 记录到 ClouDNS"""
log.info("开始检查证书验证状态...")
# 1. 查询 Cloudflare 证书状态
certs = cf_get_verification()
pending_count = 0
for cert in certs:
status = cert.get("certificate_status", "")
hosts = cert.get("hosts", [])
if status == "active":
log.info("证书已生效,覆盖域名: %s", ", ".join(hosts))
continue
if status not in ("pending_validation", "initiating"):
log.info("证书状态: %s, 跳过", status)
continue
pending_count += 1
log.info("证书待验证,覆盖域名: %s", ", ".join(hosts))
# 2. 提取需要的 TXT 记录
verifications = cert.get("verification_info", [])
if not verifications:
log.warning("无验证信息,可能证书正在初始化,稍后重试")
continue
# 按记录名分组,收集所有需要的值
needed = {} # host -> set(values)
for v in verifications:
host = extract_host(v["record_name"])
value = v["record_value"]
needed.setdefault(host, set()).add(value)
log.info("需要的 TXT 记录: %s", {h: len(vs) for h, vs in needed.items()})
# 3. 获取 ClouDNS 现有 TXT 记录
existing_records = cloudns_list_records()
existing_txt = {} # host -> list({id, record})
for rec_id, rec in existing_records.items():
if rec.get("type") == "TXT":
h = rec.get("host", "")
existing_txt.setdefault(h, []).append({
"id": rec_id,
"record": rec.get("record", ""),
})
# 4. 同步:删除过期的,添加缺失的
for host, values in needed.items():
current = existing_txt.get(host, [])
current_values = {r["record"] for r in current}
# 删除不再需要的旧记录
for rec in current:
if rec["record"] not in values:
log.info("删除过期 TXT: host=%s, value=%s...", host, rec["record"][:20])
cloudns_delete_record(rec["id"])
# 添加缺失的新记录
for value in values:
if value not in current_values:
cloudns_add_txt(host, value)
log.info("TXT 记录同步完成,等待 DNS 传播...")
if pending_count > 0:
# 5. 等待 DNS 传播后触发 Cloudflare 重新验证
time.sleep(DNS_PROPAGATION_WAIT)
cf_trigger_verification()
log.info("已触发验证,等待证书签发...")
else:
log.info("所有证书状态正常,无需操作")
def main():
if not all([CF_API_TOKEN, CF_ZONE_ID, CLOUDNS_AUTH_ID, CLOUDNS_AUTH_PASSWORD]):
log.error("请设置环境变量: CF_API_TOKEN, CF_ZONE_ID, CLOUDNS_AUTH_ID, CLOUDNS_AUTH_PASSWORD, DOMAIN")
return
sync_txt_records()
if __name__ == "__main__":
main()
定时执行
Linux / 服务器 (Crontab)
# 每天凌晨 3 点检查一次
0 3 * * * CF_API_TOKEN=xxx CF_ZONE_ID=xxx CLOUDNS_AUTH_ID=xxx CLOUDNS_AUTH_PASSWORD=xxx DOMAIN=*******.ddns-ip.net python3 /path/to/cert_auto_renew.py >> /var/log/cert_renew.log 2>&1
Termux (Android)
# 使用 termux-job-scheduler
termux-job-scheduler \
--script /path/to/cert_auto_renew.sh \
--period-ms 86400000 \
--network unmetered
# 或者用简单 cron
crontab -e
# 添加: 0 3 * * * /path/to/cert_auto_renew.sh
Shell 包装脚本 cert_auto_renew.sh:
#!/data/data/com.termux/files/usr/bin/bash
export CF_API_TOKEN="xxx"
export CF_ZONE_ID="xxx"
export CLOUDNS_AUTH_ID="xxx"
export CLOUDNS_AUTH_PASSWORD="xxx"
export DOMAIN="*******.ddns-ip.net"
python3 /path/to/cert_auto_renew.py
替代方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| API 自动同步 TXT(本文方案) | 免费、全自动、可持续 | 需要维护脚本和定时任务 |
| 手动添加 TXT 记录 | 简单直接 | 证书每 3 个月续期,不可持续 |
| Cloudflare CNAME 部分接入 | 不用改 NS | 需要 Business 计划($250/月) |
| 升级 ClouDNS 付费计划 | 可修改 NS | 有费用 |
| 换用支持修改 NS 的 DNS 服务 | 彻底解决 | 需更换域名或 DNS 提供商 |
| 不使用 Cloudflare 代理 | 无兼容性问题 | 失去 CDN/防护功能 |
注意事项
- ClouDNS 免费账户 API 限制:免费用户可以通过 API 管理 TXT 记录,但需在控制面板中手动启用 API 访问(Settings → API)
- 证书有效期 3 个月:到期前 Cloudflare 会自动发起续期并生成新的验证值,脚本需定期运行以同步最新值
- 多个 TXT 值共存:同一个 host(如
_acme-challenge.*************************)可能需要同时存在两条不同值的 TXT 记录,脚本中已处理此情况 - DNS 传播延迟:添加记录后建议等待 60 秒再触发 Cloudflare 验证,确保记录已传播
- 脚本幂等性:脚本设计为幂等操作,记录已存在且值正确时不会重复添加,可以安全地频繁运行
- 不要在 Cloudflare 面板添加
_acme-challengeTXT 记录:因为权威 NS 指向 ClouDNS,证书颁发机构不会查询 Cloudflare 的 DNS
总结
ClouDNS 免费域名 + Cloudflare 的组合看似矛盾,但通过两边的 API 桥接,可以优雅地解决证书验证问题。核心思路就是:
Cloudflare 告诉你需要什么 → 你去 ClouDNS 那边写上去 → Cloudflare 自动完成验证
这个方案完全免费,且一旦脚本部署好就可以一劳永逸地运行。

浙公网安备 33010602011771号