阿里云 SSL 证书自动续期实战(acme.sh + DNS 验证)
背景:证书只有 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 申请证书、完成域名所有权验证、下载证书,全程可以脚本化。它负责三件事:
- 向 CA 申请证书;
- 通过阿里云 DNS API 自动完成域名验证;
- 把证书部署到指定路径,并触发 Web 服务 reload。
选 DNS 验证而不是 HTTP 验证,原因很实际:DNS 验证只要求你能在域名下加一条 TXT 记录,不需要开放 80 端口、不要求服务器对外可访问,尤其适合安全组管控严格的场景。而且只有 DNS 验证支持签发通配符证书(*.example.com)。
原理:DNS 验证到底在验证什么
CA 在发证书前,要先确认「这个域名是你的」。DNS 验证用的是 ACME 协议里的 DNS-01 挑战,流程如下:
- acme.sh 让 CA 生成一个随机 token;
- acme.sh 通过阿里云 DNS API,在域名下添加一条 TXT 记录:
_acme-challenge.example.com→ 某个随机值; - CA 去公共 DNS 查询这条 TXT 记录,查到且值匹配,就认为你控制着这个域名;
- 验证通过,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,你唯一要做的,是偶尔看一眼它还在正常工作。