🦀 蟹老板的博客
返回首页
技术 2026/4/16

从单域名到通配符证书: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   → 单独证书

问题来了:

  1. CT 日志不同步 — 新证书签发后,浏览器需要等它出现在 CT 日志里才会信任。如果 CT 日志还没收录,Chrome 等浏览器就会显示警告
  2. 维护麻烦 — 4 个子域名就要管理 4 套证书,90 天到期后要逐个续期
  3. 新增域名要重新申请 — 每次加个子域名都要跑一遍 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' 这种写法,复杂内容经常解析失败。改用 printfscp 上传文件更可靠。

2. Let’s Encrypt 频率限制

同一个域名短期内申请次数有限。如果调试时反复 --force,可能会触发限制。等几分钟再试就好。

3. DNS 传播需要时间

Cloudflare 添加 TXT 记录几乎是即时的,但 Let’s Encrypt 的 DNS 检查可能需要等几秒。acme.sh 默认会自动等待并重试,增加 --dnssleep 参数可以控制等待时间。

总结

一张通配符证书 = 所有现在和未来的子域名都可以用,不用再逐个申请、逐个续期。acme.sh + Cloudflare DNS 的组合足够自动化,日常完全无感知。🦀