#SSL #acme.sh #Cloudflare #Nginx

背景

博客使用 acme.sh + Let’s Encrypt 签发 ECC 证书,原方案依赖阿里云 DNS API(dns_ali)自动验证。DNS 迁移到 Cloudflare 后需同步更新续签方式,同时记录中途遇到的一次手动续签失败的排查过程。


一、故障:续签时 Finalize 报错

错误现象

1
2
3
[ERROR] Le_OrderFinalize
[ERROR] Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: 3
[ERROR] Signing failed. Finalize code was not 200.

原因

libcurl 错误码 3 为 CURLE_URL_MALFORMAT,即 acme.sh 缓存的 ACME 订单状态中 Le_OrderFinalize URL 损坏,导致签名阶段失败。

解决

删除缓存的域名配置,强制重新发起订单:

1
2
3
4
5
6
rm -rf ~/.acme.sh/<domain>_ecc/

# 重新签发(手动 DNS 模式)
~/.acme.sh/acme.sh --issue --dns \
-d "<domain>" \
--yes-I-know-dns-manual-mode-enough-go-ahead-please

在 DNS 控制台添加提示的 _acme-challenge TXT 记录,等待解析生效后再执行:

1
2
~/.acme.sh/acme.sh --renew -d "<domain>" \
--yes-I-know-dns-manual-mode-enough-go-ahead-please

二、证书安装到 Nginx

本站多个端口(443 / 18063 / 19090 / 22017 / 4455 / 8900)共用同一份证书,统一放在:

1
2
/etc/nginx/ssl/port_18063/port_18063.pem   # fullchain
/etc/nginx/ssl/port_18063/port_18063.key # private key

安装命令:

1
2
3
4
~/.acme.sh/acme.sh --install-cert -d "<domain>" --ecc \
--key-file /etc/nginx/ssl/port_18063/port_18063.key \
--fullchain-file /etc/nginx/ssl/port_18063/port_18063.pem \
--reloadcmd "systemctl reload nginx"

验证

1
2
3
4
5
# TLS 层验证(最可靠)
curl -v https://<domain> 2>&1 | grep -E "subject|expire|SSL|CN="

# 本地文件验证
openssl x509 -noout -dates -in /etc/nginx/ssl/port_18063/port_18063.pem

注意:IDN 国际化域名(中文域名)在部分字体下 punycode 的 0(数字零)会被渲染为 O(大写字母),9 被渲染为 g,造成视觉上的"名称不匹配",实为字体问题,subjectAltName matched 才是判断依据。


三、迁移到 Cloudflare 自动续签

前置条件

  1. 域名 NS 已切换到 Cloudflare

  2. 在 CF Dashboard → My Profile → API Tokens 创建 Token,权限:

    • Zone - DNS - Edit,仅限目标域名 Zone

持久化 Token

1
2
3
# 写入 account.conf,acme.sh --cron 自动读取,无需每次 export
grep -q "CF_Token" /root/.acme.sh/account.conf 2>/dev/null \
|| echo 'CF_Token="<your_cf_api_token>"' >> /root/.acme.sh/account.conf

正式签发(绑定 dns_cf)

1
2
3
export CF_Token="<your_cf_api_token>"
/root/.acme.sh/acme.sh --issue --dns dns_cf \
-d "<domain>" --force

注意:IDN 中文域名需使用 punycode 形式传入,dns_cf 不支持直接传中文域名。

安装证书并注册 cron

1
2
3
4
5
6
7
8
/root/.acme.sh/acme.sh --install-cert -d "<domain>" --ecc \
--key-file /etc/nginx/ssl/port_18063/port_18063.key \
--fullchain-file /etc/nginx/ssl/port_18063/port_18063.pem \
--reloadcmd "systemctl reload nginx"

# 注册 cron(每天 00:00 检查,到期前 30 天自动续签)
crontab -l 2>/dev/null | grep -q acme \
|| (crontab -l 2>/dev/null; echo "0 0 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh > /dev/null") | crontab -

四、自动续签原理

1
2
3
4
5
6
7
8
9
10
11
12
每天 00:00  cron 触发 acme.sh --cron

├─ 读取 /root/.acme.sh/account.conf ← CF_Token
├─ 读取 <domain>.conf ← Le_Webroot='dns:dns_cf'、reloadcmd

├─ 距到期 > 30 天 → 跳过
└─ 距到期 ≤ 30 天 → 全自动:
1. CF API 写入 _acme-challenge TXT
2. Let's Encrypt 验证通过,签发新证书
3. CF API 删除 TXT
4. 复制到 /etc/nginx/ssl/port_18063/
5. systemctl reload nginx

验证续签配置完整性

1
2
3
4
5
6
# 确认 dns_cf 已绑定
grep Le_Webroot /root/.acme.sh/<domain>_ecc/<domain>.conf
# 期望输出: Le_Webroot='dns:dns_cf'

# 模拟 cron 执行(不强制,仅验证流程)
/root/.acme.sh/acme.sh --cron --home /root/.acme.sh

五、坑:sudo 下 $HOME 路径错误

sudo bash ssl_setup.sh 时,$HOME 解析为调用用户的家目录(如 /home/username)而非 /root,导致:

  • acme.sh 找不到

  • account.conf 写入错误位置

  • cron 命令路径错误

解决:脚本中所有 acme.sh 相关路径硬编码为 /root/.acme.sh/,不使用 $HOME~


六、前置检查脚本要点

部署前用 ssl_preflight.sh 验证:

  1. CF Token 有效性(调用 CF API 查询 Zone)

  2. acme.sh 存在于 /root/.acme.sh/

  3. 证书目录写权限

  4. Nginx 配置语法

  5. 当前证书到期时间

  6. Staging 签发测试:用 Let’s Encrypt 测试环境走完完整流程,颁发者应为 (STAGING) ...,不消耗速率限制

  7. 清理 Staging 残留,恢复生产证书状态