阿里云 SSL 证书自动续期实战(acme.sh + DNS 验证)

13 分钟

背景:证书只有 90 天,手动续期是个坑

Let's Encrypt 提供的免费 DV 证书,有效期只有 90 天。到期不续,浏览器直接挂一个刺眼的「不安全」,网站瞬间变得不可信。

手动续期有多痛苦?每 3 个月来一次:登录控制台 → 重新申请 → 下载证书 → 上传服务器 → 改配置 → reload。一次十几分钟,一年四次,还容易忘记——忘了就是一次线上事故。

更好的做法是让机器自己来。这篇记录我用 acme.sh 把整个流程自动化掉的全过程,包括中途踩的一串坑。

方案选型:acme.sh + 阿里云 DNS

先说结论:在「ECS / 自建服务器 + 阿里云 DNS」这个场景下,acme.sh 是最成熟的免费自动续期方案。

acme.sh 是一个 ACME 协议客户端。ACME(Automatic Certificate Management Environment)是 Let's Encrypt 等免费 CA 使用的标准化协议,客户端通过它向 CA 申请证书、完成域名所有权验证、下载证书,全程可以脚本化。它负责三件事:

  1. 向 CA 申请证书;
  2. 通过阿里云 DNS API 自动完成域名验证;
  3. 把证书部署到指定路径,并触发 Web 服务 reload。

选 DNS 验证而不是 HTTP 验证,原因很实际:DNS 验证只要求你能在域名下加一条 TXT 记录,不需要开放 80 端口、不要求服务器对外可访问,尤其适合安全组管控严格的场景。而且只有 DNS 验证支持签发通配符证书(*.example.com)。

原理:DNS 验证到底在验证什么

CA 在发证书前,要先确认「这个域名是你的」。DNS 验证用的是 ACME 协议里的 DNS-01 挑战,流程如下:

  1. acme.sh 让 CA 生成一个随机 token;
  2. acme.sh 通过阿里云 DNS API,在域名下添加一条 TXT 记录:_acme-challenge.example.com → 某个随机值;
  3. CA 去公共 DNS 查询这条 TXT 记录,查到且值匹配,就认为你控制着这个域名;
  4. 验证通过,CA 签发证书;acme.sh 再删掉这条临时记录。

理解了这条链路,后面很多报错就一眼能看懂。比如签发时如果卡在:

Adding TXT value: xxxxx for domain: _acme-challenge.example.com
Error adding TXT record to domain: _acme-challenge.example.com

第一行说明 acme.sh 已经成功调用了阿里云 DNS API;第二行说明加 TXT 记录这一步被阿里云拒绝了——问题几乎都在凭证或权限上,而不是 acme.sh 本身。

实战步骤

下面按顺序走一遍完整流程。域名、IP 等敏感信息都换成了占位符,实际使用时替换成自己的。

1. 创建 RAM 子账号

不要用主账号 AccessKey。主账号一旦泄露等于整个账号沦陷,只给子账号最小权限,泄露了损失也小。

在阿里云 RAM 控制台新建一个用户,勾选「OpenAPI 调用访问」生成 AccessKey,然后单独授权 AliyunDNSFullAccess 权限。记住 AccessKeyId 和 AccessKeySecret,后面用。

这一步最容易漏的是「授权」——很多人建了用户、生成了 Key,却忘了挂权限,导致后面签发时加不上 TXT 记录。

2. 安装 acme.sh(国内服务器走 gitee)

官方安装命令是 curl https://get.acme.sh | sh,但这个脚本会去拉 GitHub 的 raw.githubusercontent.com,国内服务器大概率连接重置。改用官方的 gitee 镜像:

git clone https://gitee.com/neilpang/acme.sh.git
cd acme.sh
./acme.sh --install -m your-email@example.com

装完 acme.sh 在 ~/.acme.sh/ 目录下,--install 会顺手配好每日检查的 cron 任务。

3. 签发证书

先导出凭证环境变量,再签发。这里以裸域名 + www 两个域名一起签为例:

export Ali_Key="你的AccessKeyId"
export Ali_Secret="你的AccessKeySecret"

acme.sh --issue --dns dns_ali -d example.com -d www.example.com

成功后日志末尾会出现 Cert success,证书生成在 ~/.acme.sh/example.com_ecc/ 目录(ECC 证书)。

4. 部署证书并配置自动 reload

用 --install-cert 把证书复制到 Nginx 要读的目录,并注册一条 reload 命令。关键:路径要和你实际部署方式对齐,Docker 部署见后面的坑 5。

acme.sh --install-cert -d example.com \
  --key-file /etc/ssl/cert.key \
  --fullchain-file /etc/ssl/cert.pem \
  --reloadcmd "docker exec your-project-nginx-1 nginx -s reload"

--install-cert 只复制文件、注册命令,首次不会自动执行 reloadcmd,需要自己 reload 一次;之后每次续期,acme.sh 才会自动跑这条 reload。

踩坑记录

这部分是本次折腾的精华,几乎每个坑都卡了我一会儿。

坑 1:raw.githubusercontent.com 连接重置

现象:执行 curl https://get.acme.sh | sh 报 Connection reset by peer。

原因:安装脚本要从 GitHub 拉代码,国内服务器直连 raw.githubusercontent.com 经常被重置,跟 acme.sh 本身无关。

解决:换 gitee 镜像,用 git clone https://gitee.com/neilpang/acme.sh.git 再 --install。装好后 acme.sh 的自动升级默认仍走 GitHub,可以关掉自动升级避免报错,不影响证书续期。

坑 2:RAM 权限给错,TXT 记录加不上

现象:签发时报 Error adding TXT record。

原因:RAM 子账号建了、Key 也生成了,但没授权 AliyunDNSFullAccess。API 能调用,但没权限操作解析记录。

解决:去 RAM 控制台给子账号补上 AliyunDNSFullAccess 权限,重新签发即可。排查时可以用 --debug 2 看阿里云返回的具体错误码。

坑 3:多行命令反斜杠续行失效

现象:把 --install-cert 拆成多行写,报 --key-file: command not found。

原因:每行结尾的 \ 后面多了一个空格,续行符失效,shell 把 --key-file 当成了独立命令执行。

解决:要么写成一整行,要么确保 \ 是行尾最后一个字符(后面不能有任何空格)。新手建议直接写单行,最省事。

坑 4:systemctl reload 报 Unit not found

现象:reload 时报 Failed to reload nginx.service: Unit nginx.service not found。

原因:Nginx 是 Docker 部署的,systemd 里根本没有 nginx 这个服务单元。

解决:reload 命令改成进容器执行:

docker exec your-project-nginx-1 nginx -s reload

先 docker ps 确认容器名,再用 docker exec <容器名> nginx -t 单独验证这条命令本身能跑通。

坑 5:证书路径写错,Nginx 读不到

现象:证书部署了、reload 也成功了,但访问还是旧证书或报证书错误。

原因:证书复制到了宿主机某个路径,但 Docker 部署时,容器里的 Nginx 看不到宿主机路径,除非你挂载了对应目录。我一开始按网上的教程写 /etc/nginx/ssl/...,结果实际项目里 docker-compose 挂载的是 /etc/ssl/cert.pem。

解决:先看自己的挂载配置再定路径。用 docker-compose.yml 里的 volumes 和 Nginx 配置里的 ssl_certificate 两相对照,证书必须放到「宿主机被挂载、且 Nginx 配置里引用的那个路径」。查容器内实际读到哪个文件,可以用:

docker exec your-project-nginx-1 nginx -T 2>&1 | grep ssl_certificate

坑 6:域名 A 记录指错 IP

现象:www 域名能访问,裸域名访问不了。

原因:换过服务器之后,只改了 www 的 A 记录,忘了改 @(裸域名)的记录,它还指向旧 IP。

解决:去云解析 DNS 控制台,把 @ 记录的 A 值改成当前服务器的公网 IP,和 www 保持一致。改完用公共 DNS 验证。

坑 7:被内网 DNS 误导,差点误判

现象:在服务器上用 nslookup example.com 查,返回 Can't find example.com: No answer,第一反应以为是没配 A 记录。

原因:服务器用的是内网 DNS,它解析不到自己的域名,但外部用户用的是公共 DNS,不受影响。

解决:换公共 DNS 复测(nslookup example.com 223.5.5.5),发现 A 记录其实一直在。教训是:排查域名解析问题,别只信内网 DNS 的结果,用公共 DNS 交叉验证一下。

验证自动续期是否真的就绪

续期要能全自动,光签成功不够,得确认三样东西都在位:

① cron 定时任务已配置(每天自动检查)

crontab -l | grep acme

② 阿里云凭证已持久化(续期时能自动读取,不用再手动 export)

grep -c "SAVED_Ali_" ~/.acme.sh/account.conf

返回 2 说明 Key 和 Secret 都存进配置了。

③ reloadcmd 已绑定(续期成功后自动 reload)

acme.sh --list

能看到证书以及它绑定的 reload 命令。

最后干跑一次续期流程,提前排雷:

acme.sh --cron --debug 2>&1 | tail -20

证书没到期时它会显示 Skip,不会真的重新签发,这一步的价值在于:如果凭证或权限有问题,这里会当场暴露,而不是等到 3 个月后续期失败才发现。

验证证书是否生效,可以在服务器上用 openssl 看有效期:

echo | openssl s_client -servername example.com -connect localhost:443 2>/dev/null | openssl x509 -noout -dates

notBefore 是今天、notAfter 是约 3 个月后,就说明新证书已生效。浏览器里点地址栏小锁 → 证书,看「有效期起始日期」也能确认。

总结

整个方案的核心是让续期这件事完全脱离人工。归纳下来:

  • acme.sh 负责跑腿:向 CA 申请、用阿里云 DNS API 完成验证、部署证书、触发 reload,全程脚本化。
  • 最小权限原则:RAM 子账号只授 AliyunDNSFullAccess,泄露风险可控。
  • 国内环境的两个绕行:安装走 gitee 镜像,避开 GitHub 连接重置。
  • 部署方式决定路径和 reload 命令:Docker 部署时证书路径要对齐挂载点,reload 用 docker exec 而不是 systemctl。
  • 自动续期的三要素:cron 任务、持久化凭证、reloadcmd,缺一个续期就会断。

最容易被忽略的其实是「验证」——不是签完就完了,而是确认 cron、凭证、reloadcmd 三样都在,并干跑一次 --cron 提前排雷。做完这些,以后每 3 个月证书快到期时,机器会自己续好、自己 reload,你唯一要做的,是偶尔看一眼它还在正常工作。

南一

前端工程师,在这里整理面试知识,也记录从零做一个网站的过程。

关于本站 →

评论

加载评论中…