ClouDNS 免费域名托管到 Cloudflare 后证书申请失败的自动化解决方案

ClouDNS 免费域名托管到 Cloudflare 后证书申请失败的自动化解决方案

当你把 ClouDNS 的免费域名(如 ddns-ip.net 子域名)添加到 Cloudflare 时,由于免费用户无法修改 SOA 和 NS 记录,Cloudflare 的 SSL 证书验证会失败。本文介绍如何利用两边的 API 实现证书自动签发。

背景

ClouDNS 提供免费的 DNS 托管服务,支持 ddns-ip.netcloudns.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 值,都需要添加。

手动解决方案

如果只是偶尔需要,可以手动操作:

  1. 登录 ClouDNS 控制面板
  2. 进入域名的 DNS 区域
  3. 逐条添加 TXT 记录
Host 类型
_acme-challenge TXT lmBBknDTmOKc2FPbLAzgamZqclHz7h_IljWTCRQ-81c
_acme-challenge.************************* TXT U0U-g2ZMKja7iRBMAT4IhfDP9ZO7pfBOiJ7wTVRLVAs
_acme-challenge.************************* TXT l8xfO5L5UTNPJAK7eOAsG8zjJDBCoW5_u-x13YOo2zg
  1. 等待 DNS 传播,用 dig 验证:
dig @ns1.cloudns.net TXT _acme-challenge.*******.ddns-ip.net
dig @ns1.cloudns.net TXT _acme-challenge.*************************.*******.ddns-ip.net
  1. 回到 Cloudflare 控制面板检查证书状态

问题:证书每 3 个月续期一次,每次续期都会生成新的验证值,手动操作不可持续。

自动化解决方案

整体架构

┌─────────────┐     1. 查询证书状态     ┌──────────────┐
│   定时脚本   │ ────────────────────→ │  Cloudflare  │
│  (Cron Job)  │ ←──────────────────── │     API      │
│             │     2. 返回TXT验证信息   │              │
│             │                        └──────────────┘
│             │
│             │     3. 添加/更新TXT记录   ┌──────────────┐
│             │ ────────────────────→ │   ClouDNS    │
│             │ ←──────────────────── │     API      │
│             │     4. 确认操作成功      │              │
└─────────────┘                        └──────────────┘
       │
       │  5. 等待DNS传播 + Cloudflare自动验证
       ▼
  证书签发成功

前置准备

Cloudflare 侧

  1. 创建 API Token:Cloudflare 控制面板 → My Profile → API Tokens → Create Token
  2. 所需权限:Zone:SSL and Certificates:ReadZone:SSL and Certificates:Edit
  3. 记录下 Zone ID(在域名概览页右下角)

ClouDNS 侧

  1. 登录 ClouDNS 控制面板 → Settings → API
  2. 启用 API 访问,获取 auth-idauth-password
  3. 免费用户也可使用 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/防护功能

注意事项

  1. ClouDNS 免费账户 API 限制:免费用户可以通过 API 管理 TXT 记录,但需在控制面板中手动启用 API 访问(Settings → API)
  2. 证书有效期 3 个月:到期前 Cloudflare 会自动发起续期并生成新的验证值,脚本需定期运行以同步最新值
  3. 多个 TXT 值共存:同一个 host(如 _acme-challenge.*************************)可能需要同时存在两条不同值的 TXT 记录,脚本中已处理此情况
  4. DNS 传播延迟:添加记录后建议等待 60 秒再触发 Cloudflare 验证,确保记录已传播
  5. 脚本幂等性:脚本设计为幂等操作,记录已存在且值正确时不会重复添加,可以安全地频繁运行
  6. 不要在 Cloudflare 面板添加 _acme-challenge TXT 记录:因为权威 NS 指向 ClouDNS,证书颁发机构不会查询 Cloudflare 的 DNS

总结

ClouDNS 免费域名 + Cloudflare 的组合看似矛盾,但通过两边的 API 桥接,可以优雅地解决证书验证问题。核心思路就是:

Cloudflare 告诉你需要什么 → 你去 ClouDNS 那边写上去 → Cloudflare 自动完成验证

这个方案完全免费,且一旦脚本部署好就可以一劳永逸地运行。

posted @ 2026-04-12 11:48  masx200  阅读(26)  评论(0)    收藏  举报