Certbot 证书自动化教程 / 第 5 章:DNS 验证
第 5 章:DNS 验证
5.1 DNS 验证概述
DNS 验证(DNS-01 Challenge)是通过在域名的 DNS 记录中添加特定的 TXT 记录来证明域名控制权的方式。这是申请通配符证书的唯一方式,也适用于无法开放 80 端口的场景。
为什么需要 DNS 验证
| 场景 | HTTP-01 | DNS-01 |
|---|---|---|
通配符证书 *.example.com | ❌ 不支持 | ✅ 支持 |
| 80 端口不可用 | ❌ 需要 | ✅ 不需要 |
| 内网服务器 | ❌ 不支持 | ✅ 支持 |
| CDN 后面的服务器 | ❌ 验证困难 | ✅ 不受影响 |
| 多层 NAT 环境 | ❌ 验证困难 | ✅ 不受影响 |
DNS-01 验证流程
1. Certbot 生成验证值
值: <base64url-encoded-SHA256-of-key-authorization>
2. Certbot(或手动)添加 DNS TXT 记录
名称: _acme-challenge.example.com
类型: TXT
值: <generated-value>
3. Let's Encrypt 查询 DNS
dig TXT _acme-challenge.example.com
4. 验证记录值匹配 → 域名控制权确认
DNS 插件工作方式
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Certbot │ │ DNS 插件 │ │ DNS 提供商 │
│ │────>│ (cloudflare/ │────>│ (Cloudflare/ │
│ 生成验证值 │ │ route53 等) │ │ Route53 等) │
└──────────────┘ └──────────────┘ └──────────────┘
│
▼
添加 _acme-challenge
TXT 记录
│
▼
┌──────────────┐ 查询 DNS ┌──────────────┐
│ Let's Encrypt│ <────────────────────│ DNS 服务器 │
│ 服务器 │ │ │
└──────────────┘ └──────────────┘
5.2 手动 DNS 验证
不使用 DNS 插件时,可以通过手动方式完成 DNS 验证。
手动申请通配符证书
# 手动 DNS 验证
sudo certbot certonly --manual \
--preferred-challenges dns \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com
执行后 Certbot 会提示:
Please deploy a DNS TXT record under the name
_acme-challenge.example.com with the following value:
abcdef1234567890abcdef1234567890abcdef12
Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
手动验证步骤
# 1. 登录 DNS 管理面板
# 2. 添加 TXT 记录:
# 名称: _acme-challenge
# 类型: TXT
# 值: abcdef1234567890abcdef1234567890abcdef12
# TTL: 60
# 3. 等待 DNS 记录生效(通常 1-5 分钟)
dig TXT _acme-challenge.example.com
# 4. 确认记录已生效后,回到终端按 Enter 继续
注意: 如果同时申请
example.com和*.example.com,Certbot 会要求添加两条不同的 TXT 记录到_acme-challenge.example.com。
手动验证的局限性
- 无法自动续期(每次续期都需要手动添加 DNS 记录)
- 操作繁琐,容易出错
- 不适合生产环境的自动化管理
5.3 Cloudflare DNS 插件
Cloudflare 是最常用的 DNS 服务提供商之一,Certbot 提供了官方的 Cloudflare DNS 插件。
安装插件
# Snap 安装
sudo snap install certbot-dns-cloudflare
# 或 pip 安装
pip install certbot-dns-cloudflare
获取 Cloudflare API 凭证
方式一:API Token(推荐)
- 登录 Cloudflare Dashboard
- 进入 My Profile → API Tokens
- 点击 Create Token
- 选择 Edit zone DNS 模板
- 配置权限:
- Zone - DNS - Edit
- Zone - Zone - Read
- 指定区域(Zone)为你的域名
- 创建并保存 Token
方式二:Global API Key(不推荐)
- 登录 Cloudflare Dashboard
- 进入 My Profile → API Tokens
- 查看 Global API Key
配置凭证文件
# 创建凭证文件
sudo mkdir -p /etc/letsencrypt/cloudflare
sudo tee /etc/letsencrypt/cloudflare/credentials.ini > /dev/null << 'EOF'
# Cloudflare API Token(推荐)
dns_cloudflare_api_token = YOUR_API_TOKEN_HERE
EOF
# 或使用 Global API Key
# dns_cloudflare_email = your-email@example.com
# dns_cloudflare_api_key = YOUR_GLOBAL_API_KEY
# 设置权限(仅 root 可读)
sudo chmod 600 /etc/letsencrypt/cloudflare/credentials.ini
使用 Cloudflare 插件申请证书
# 申请普通证书
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d example.com \
-d www.example.com \
--agree-tos \
--email admin@example.com \
--non-interactive
# 申请通配符证书
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com \
--non-interactive
# 等待 DNS 传播时间(默认 10 秒,可调整)
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
--dns-cloudflare-propagation-seconds 30 \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com \
--non-interactive
Cloudflare 插件参数
| 参数 | 说明 | 默认值 |
|---|---|---|
--dns-cloudflare | 启用 Cloudflare 插件 | - |
--dns-cloudflare-credentials | 凭证文件路径 | - |
--dns-cloudflare-propagation-seconds | DNS 传播等待时间 | 10 秒 |
5.4 AWS Route53 DNS 插件
安装插件
# pip 安装(Snap 不提供此插件)
pip install certbot-dns-route53
配置 AWS 凭证
# 方式一:环境变量
export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_KEY"
# 方式二:AWS 凭证文件
mkdir -p ~/.aws
cat > ~/.aws/credentials << EOF
[default]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY
EOF
# 方式三:IAM 角色(EC2 实例推荐)
# 无需配置,自动使用实例角色
所需 IAM 权限
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:GetChange",
"route53:ChangeResourceRecordSets",
"route53:ListHostedZonesByName"
],
"Resource": [
"arn:aws:route53:::hostedzone/*",
"arn:aws:route53:::change/*"
]
}
]
}
使用 Route53 插件申请证书
# 申请通配符证书
sudo certbot certonly \
--dns-route53 \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com \
--non-interactive
Route53 插件参数
| 参数 | 说明 |
|---|---|
--dns-route53 | 启用 Route53 插件 |
注意: Route53 插件不需要额外的凭证文件,使用 AWS SDK 的默认凭证链。
5.5 其他 DNS 插件
DigitalOcean
# 安装
sudo snap install certbot-dns-digitalocean
# 凭证文件
sudo tee /etc/letsencrypt/digitalocean/credentials.ini > /dev/null << 'EOF'
dns_digitalocean_token = YOUR_DIGITALOCEAN_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/digitalocean/credentials.ini
# 申请证书
sudo certbot certonly \
--dns-digitalocean \
--dns-digitalocean-credentials /etc/letsencrypt/digitalocean/credentials.ini \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com
Google Cloud DNS
# 安装
pip install certbot-dns-google
# 凭证文件(Google Cloud 服务账号 JSON 密钥)
# 申请证书
sudo certbot certonly \
--dns-google \
--dns-google-credentials /path/to/credentials.json \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com
RFC 2136(通用 DNS 更新)
# 安装
pip install certbot-dns-rfc2136
# 凭证文件
sudo tee /etc/letsencrypt/rfc2136/credentials.ini > /dev/null << 'EOF'
dns_rfc2136_server = dns-server-ip
dns_rfc2136_port = 53
dns_rfc2136_name = tsig-key-name
dns_rfc2136_secret = tsig-key-secret
dns_rfc2136_algorithm = HMAC-SHA256
EOF
sudo chmod 600 /etc/letsencrypt/rfc2136/credentials.ini
# 申请证书
sudo certbot certonly \
--dns-rfc2136 \
--dns-rfc2136-credentials /etc/letsencrypt/rfc2136/credentials.ini \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com
Hetzner DNS
# 安装
pip install certbot-dns-hetzner
# 凭证文件
sudo tee /etc/letsencrypt/hetzner/credentials.ini > /dev/null << 'EOF'
dns_hetzner_api_token = YOUR_HETZNER_API_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/hetzner/credentials.ini
# 申请证书
sudo certbot certonly \
--dns-hetzner \
--dns-hetzner-credentials /etc/letsencrypt/hetzner/credentials.ini \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com
5.6 通配符证书
什么是通配符证书
通配符证书(Wildcard Certificate)可以保护一个域名及其所有同级子域名。例如,*.example.com 可以保护:
www.example.comapi.example.commail.example.comblog.example.com
但不能保护:
example.com(需要单独添加)sub.www.example.com(不能匹配多级子域名)
申请通配符证书
# 同时申请根域名和通配符
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com \
--non-interactive
通配符证书文件
# 查看证书信息
sudo certbot certificates --cert-name example.com
# 证书文件位置
/etc/letsencrypt/live/example.com/
├── cert.pem
├── chain.pem
├── fullchain.pem
└── privkey.pem
通配符证书的续期
# 通配符证书续期与普通证书相同
sudo certbot renew --cert-name example.com
# 续期时会自动使用相同的 DNS 插件和凭证
# 需要确保 DNS 凭证文件仍然有效
5.7 DNS 传播时间
DNS 记录的传播需要时间,不同提供商的速度差异较大。
| DNS 提供商 | 平均传播时间 | 推荐等待时间 |
|---|---|---|
| Cloudflare | 5-10 秒 | 10 秒 |
| AWS Route53 | 30-60 秒 | 60 秒 |
| Google Cloud DNS | 30-120 秒 | 60 秒 |
| DigitalOcean | 30-60 秒 | 60 秒 |
| GoDaddy | 60-600 秒 | 300 秒 |
| 传统 DNS 托管 | 300-3600 秒 | 600 秒 |
设置传播等待时间
# Cloudflare(传播快,10 秒通常足够)
--dns-cloudflare-propagation-seconds 10
# Route53(默认无参数,使用插件默认值)
# 通常 30-60 秒
# RFC 2136
--dns-rfc2136-propagation-seconds 60
验证 DNS 记录
# 查询 TXT 记录
dig TXT _acme-challenge.example.com +short
# 使用 Google Public DNS 查询
dig TXT _acme-challenge.example.com @8.8.8.8 +short
# 使用 Cloudflare DNS 查询
dig TXT _acme-challenge.example.com @1.1.1.1 +short
5.8 DNS 验证自动脚本
自定义 DNS API 脚本
如果 Certbot 没有提供你的 DNS 服务提供商的官方插件,可以使用 --manual-auth-hook 和 --manual-cleanup-hook 实现自动化。
#!/bin/bash
# file: /usr/local/bin/dns-add-record.sh
# 描述:通过 API 添加 DNS TXT 记录
DOMAIN="$CERTBOT_DOMAIN"
VALIDATION="$CERTBOT_VALIDATION"
TOKEN="_acme-challenge"
# 调用 DNS API 添加 TXT 记录
# 示例:使用 curl 调用自定义 API
curl -X POST "https://dns-api.example.com/records" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"TXT\",
\"name\": \"$TOKEN.$DOMAIN\",
\"value\": \"$VALIDATION\",
\"ttl\": 60
}"
# 等待 DNS 传播
sleep 30
#!/bin/bash
# file: /usr/local/bin/dns-remove-record.sh
# 描述:通过 API 删除 DNS TXT 记录
DOMAIN="$CERTBOT_DOMAIN"
TOKEN="_acme-challenge"
# 调用 DNS API 删除 TXT 记录
curl -X DELETE "https://dns-api.example.com/records" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"TXT\",
\"name\": \"$TOKEN.$DOMAIN\"
}"
使用自定义 Hook
sudo certbot certonly --manual \
--preferred-challenges dns \
--manual-auth-hook /usr/local/bin/dns-add-record.sh \
--manual-cleanup-hook /usr/local/bin/dns-remove-record.sh \
-d example.com \
-d "*.example.com" \
--agree-tos \
--email admin@example.com \
--non-interactive
提示: 使用
--manual-auth-hook和--manual-cleanup-hook后,DNS 验证可以完全自动化,支持自动续期。
5.9 DNS 验证排错
常见错误
错误 1:TXT 记录未生效
DNS problem: NXDOMAIN looking up TXT for _acme-challenge.example.com
解决方案:
# 检查记录是否已添加
dig TXT _acme-challenge.example.com +short
# 等待更长时间
# 检查 DNS 记录名称是否正确(不要包含根域名)
# _acme-challenge.example.com ✅
# _acme-challenge.example.com.example.com ❌
错误 2:API 凭证无效
Error finding zone for example.com: Unauthorized
解决方案:
# 检查凭证文件
cat /etc/letsencrypt/cloudflare/credentials.ini
# 确认权限
ls -la /etc/letsencrypt/cloudflare/credentials.ini
# 应该是 -rw------- (600)
# 验证 API Token 是否有效
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type:application/json"
错误 3:DNS 传播时间不足
DNS problem: DNS record for _acme-challenge.example.com did not propagate in time
解决方案:
# 增加传播等待时间
--dns-cloudflare-propagation-seconds 30
# 或手动确认记录已生效后再继续
5.10 DNS 验证最佳实践
- 使用 API Token 而非 Global API Key: 权限最小化,安全性更高
- 凭证文件权限: 设置为
600,仅 root 可读 - 合理设置传播等待时间: 根据 DNS 提供商的速度调整
- 使用自动化 Hook: 将手动 DNS 验证转化为自动化流程
- 测试续期: 使用
--dry-run验证 DNS 插件的续期流程
注意事项
- 通配符证书必须使用 DNS 验证: HTTP-01 和 TLS-ALPN-01 都不支持通配符
- API Token 安全: 不要在脚本中硬编码 Token,使用凭证文件
- DNS 提供商锁定: 使用第三方 DNS 插件时,切换 DNS 提供商需要更换插件和凭证
- 多级通配符不支持:
*.example.com不能匹配sub.www.example.com