从单域名到通配符证书:Let's Encrypt + acme.sh + Cloudflare 全记录
用 acme.sh 通过 Cloudflare DNS API 一次性申请 *.example.com 通配符证书,覆盖所有子域名,一劳永逸解决 SSL 证书管理问题。
🔐 从单域名到通配符证书
博客上线后,Let’s Encrypt 证书一直有个小问题:浏览器时不时会显示红色警告。排查了一圈,发现问题出在 Certificate Transparency(CT)日志上。顺藤摸瓜,干脆升级成了通配符证书,一劳永逸。
起因:为什么不用单独证书
之前用 certbot 单独给每个子域名申请证书:
www.example.com → 单独证书
app.example.com → 单独证书
api.example.com → 单独证书
cdn.example.com → 单独证书
问题来了:
- CT 日志不同步 — 新证书签发后,浏览器需要等它出现在 CT 日志里才会信任。如果 CT 日志还没收录,Chrome 等浏览器就会显示警告
- 维护麻烦 — 4 个子域名就要管理 4 套证书,90 天到期后要逐个续期
- 新增域名要重新申请 — 每次加个子域名都要跑一遍 certbot
方案选型
最终选择了 acme.sh + Cloudflare DNS API 的组合:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Certbot + HTTP 验证 | 简单,无需改 DNS | 需要 80 端口,FRP 穿透略麻烦 |
| Certbot + DNS 验证 | 通配符支持 | 还是得手动续期 |
| acme.sh + Cloudflare DNS | 通配符 + 自动续期 + 纯 Shell | 需要 Cloudflare API Token |
Cloudflare DNS 验证(DNS-01)的原理很简单:CA 会让你在 DNS 里加一条 TXT 记录,证明你拥有这个域名。acme.sh 支持直接调用 Cloudflare API 自动添加/删除这条记录,全程无需人工干预。
实施步骤
1. 创建 Cloudflare API Token
登录 Cloudflare → 我的个人资料 → API Token → 创建令牌:
- 令牌名称:任意,如
acme-dns - 账户权限:区域 → 编辑
- 区域资源:包括 → 所有区域(或者只选
example.com)
创建后保存 Token,格式是 cfut_xxxxxxxxxx。
2. 服务器安装 acme.sh
# 服务器上一行命令搞定
curl https://get.acme.sh | sh -s email=your@email.com
安装完成后重登 SSH 让 alias 生效。
3. 申请通配符证书
export CF_Token='cfut_your_token_here'
acme.sh --issue \
-d example.com \
-d '*.example.com' \
--dns dns_cf \
--keylength ec-256 \
--force
参数说明:
--dns dns_cf:使用 Cloudflare DNS API 验证--keylength ec-256:用 ECDSA P-256 密钥,比 RSA 更快更小--force:强制重新签发(调试时用)
acme.sh 会自动在 Cloudflare 添加两条 TXT 记录:
_acme-challenge.example.com
_acme-challenge.example.com
等 CA 验证通过后删除。整个过程约 30 秒。
4. 安装证书到 Nginx 目录
# 创建目标目录
mkdir -p /etc/letsencrypt/wildcard.example.com
# 安装证书并配置自动 reload
acme.sh --install-cert \
-d example.com \
-d '*.example.com' \
--ecc \
--fullchain-file /etc/letsencrypt/wildcard.example.com/fullchain.pem \
--key-file /etc/letsencrypt/wildcard.example.com/privkey.pem \
--reloadcmd 'systemctl reload nginx'
5. 更新所有 Nginx 配置
原来每个子域名的配置里,SSL 证书路径都指向各自的单独证书:
# 旧配置(每个子域名各一份)
ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
批量替换成通配符证书:
sed -i 's|/etc/letsencrypt/live/www.example.com|/etc/letsencrypt/wildcard.example.com|g' \
/etc/nginx/sites-available/www.example.com
# 重复替换 app、api、cdn 的配置...
然后重载 Nginx:
systemctl reload nginx
验证结果
# 检查所有子域名是否共用同一证书
for domain in www app api cdn; do
echo | openssl s_client \
-connect $domain.example.com:443 \
-servername $domain.example.com 2>/dev/null \
| openssl x509 -noout -serial
done
输出全部一致:
serial=058ED9D93C2F3431C421AD297283200B0DD7
serial=058ED9D93C2F3431C421AD297283200B0DD7
serial=058ED9D93C2F3431C421AD297283200B0DD7
serial=058ED9D93C2F3431C421AD297283200B0DD7
一张证书覆盖全部 4 个子域名 ✅
后续维护
acme.sh 会自动在证书到期前 30 天续期,并执行 --reloadcmd 重载 Nginx,完全不用人工干预。
查看已安装的证书:
acme.sh --list
查看续期日志:
tail -f /root/.acme.sh/acme.sh.log
踩坑记录
1. SSH heredoc 容易出错
用 ssh host 'cat > file << EOF ... EOF' 这种写法,复杂内容经常解析失败。改用 printf 或 scp 上传文件更可靠。
2. Let’s Encrypt 频率限制
同一个域名短期内申请次数有限。如果调试时反复 --force,可能会触发限制。等几分钟再试就好。
3. DNS 传播需要时间
Cloudflare 添加 TXT 记录几乎是即时的,但 Let’s Encrypt 的 DNS 检查可能需要等几秒。acme.sh 默认会自动等待并重试,增加 --dnssleep 参数可以控制等待时间。
总结
一张通配符证书 = 所有现在和未来的子域名都可以用,不用再逐个申请、逐个续期。acme.sh + Cloudflare DNS 的组合足够自动化,日常完全无感知。🦀