最近用 Certbot 给域名签发通配符证书,DNS 托管在 HE.NET,用 API 自动验证。本以为很简单,结果连环踩坑,记录一下排错过程。
最初的错误脚本
这是我最开始写的 /opt/dns_auth.sh,里面集中了本次所有的坑:
#!/bin/bash
set -euo pipefail
# 【错误1】DNS-01验证根本没有 CERTBOT_TOKEN 环境变量,只有 CERTBOT_VALIDATION
KEYAUTH=$CERTBOT_TOKEN
# 创建或更新 TXT 记录
curl -s -X POST "https://dyn.dns.he.net/nic/update" \
# 【错误2】hostname 写死了,同时申请主域名和通配符域名时,第二次验证应该覆盖第一次的记录
-d "hostname=_acme-challenge.test.local" -d "password=password" \
# 【错误3】把验证码传给了 ttl 参数,根本没传 txt 参数,导致写入 DNS 的值完全错乱
-d "ttl=${KEYAUTH}" | jq .
# 【错误4】HE.NET 返回纯文本,用 jq 解析会报错,且 set -e 会导致脚本在这里直接退出,后续的程序无法正常运行。
踩坑过程与状态码
第 1 次尝试:变量未定义
脚本在第一行就挂了,因为 CERTBOT_TOKEN 不存在,set -u 直接拦截。
Hook '--manual-auth-hook' for test.local reported error code 1
Hook '--manual-auth-hook' for test.local ran with error output:
/opt/dns_auth.sh: line 7: CERTBOT_TOKEN: unbound variable
...
Detail: Incorrect TXT record "GIh97XkH-ICQBLEfubTEEmR2j8a-3OXKbl21WYP9PHE" found at _acme-challenge.test.local
第 2 次尝试:API 参数写反 + jq 报错
修改变量名后,因为把值传给了 ttl 没传 txt,同时在dns.he.net给对应的记录配置DDNS的时候在DNS 记录那里写入了ddns_token的值;同时 jq 解析纯文本报错,导致脚本中断,没有等待 DNS 生效。
真正配置ddns_token的地方是在记录名配置窗口勾选了”Enable entry for dynamic dns”之后在右边会出现一个刷新的图标

然后在弹出的窗口输入或者生成一个ddns_token,点击蓝色按钮”Generate a key”就是生成一个密钥,如果不点击也可以直接在两个黄色框里面分别输入*两次相同的内容*1作为密钥 ,然后点击绿色按钮提交即可。
**内容仅限英文和数字**

Hook '--manual-auth-hook' for test.local reported error code 1
Hook '--manual-auth-hook' for test.local ran with error output:
jq: parse error: Invalid numeric literal at line 1, column 5
...
Detail: Incorrect TXT record "password" found at _acme-challenge.test.local
Detail: Incorrect TXT record "Acme challenge key" found at _acme-challenge.test.local
第 3 次尝试:触发限速
频繁试错,1 小时内失败 5 次,触发 Let’s Encrypt 限速机制,只能干等。
An unexpected error occurred:
too many failed authorizations (5) for "*.test.local" in the last 1h0m0s, retry after 2026-05-11 14:57:21 UTC
第 4 次尝试:成功
修正所有错误,删除 jq,使用 txt=${VALIDATION},动态拼接 hostname,加上 sleep 30。

最终的正确脚本
/opt/dns_auth.sh:
#!/usr/bin/env bash
set -euo pipefail
# 获取验证码
VALIDATION=$CERTBOT_VALIDATION
DOMAIN=$CERTBOT_DOMAIN
# 提取主域名:把 *.test.local 转换为 test.local
if [[ "$DOMAIN" == *.test.local ]]; then
REAL_DOMAIN="${DOMAIN#*.}"
else
REAL_DOMAIN=$DOMAIN
fi
# 动态拼接 hostname,避免同时签发主域名和通配符时记录互相覆盖
RECORD_NAME="_acme-challenge.${REAL_DOMAIN}"
# 正确将验证码传给 txt 参数,去掉 jq
curl -s -X POST "https://dyn.dns.he.net/nic/update" \
-d "hostname=${RECORD_NAME}" \
-d "password=你的DDNS密码" \
-d "txt=${VALIDATION}"
# 等待 DNS 生效
sleep 30
签发命令:
certbot certonly --manual \
--preferred-challenges=dns \
--manual-auth-hook /opt/dns_auth.sh \
-d test.local \
-d *.test.local
几句讲解
- DNS-01 验证只有
CERTBOT_VALIDATION,没有CERTBOT_TOKEN。 - HE.NET 的 API 用
txt=传验证码,别传错参数。 - API 返回值是纯文本,别用
jq解析,配合set -e会导致脚本意外中断。 - 同时签主域名和通配符时,
hostname一定要动态拼接,否则两次验证的记录会互相覆盖。 - 排错时先手动用
curl测 API 是否返回good,别直接拿 Certbot 试,容易触发限速。
- **内容仅限英文和数字** ↩︎
