你还在每 90 天手动续一次 SSL 证书吗?如果是,我猜你已经设了好几个“证书还有 XX 天过期”的闹钟,甚至可能哪天手一抖忘了,第二天就迎来浏览器那个刺眼的红色警告页面。我自己手上十几个域名跑着 HTTPS 服务,以前每逢证书临期就得登录后台、点续期、下载证书、传服务器、reload Nginx,流程熟练得像流水线一样。后来我把这套活儿交给了 acme.sh 配合阿里云 DNS API,让证书续期变成服务器自己每天检查、自动签发、自动部署,我才算真的从这件事里解脱出来。
这篇文章不是那种“复制粘贴能跑就行”的教程,它会带你从手动续期的痛点出发,把 ACME 两种校验方式的原理讲清楚,再一步步演示如何在阿里云创建 RAM 子用户、配置 DNS API 权限、用 acme.sh 签发证书、把证书装进 Nginx,最后验证自动续期链路是否真的能跑通。无论你是刚接触 HTTPS 的小白,还是已经被手动续期折磨到想写脚本的运维,这套方案都适合你。看完之后,证书过期这件事基本可以从你的待办清单里划掉了。
1. 手动续期到底痛在哪,为什么必须自动化
1.1 90天有效期和人类记性之间的矛盾
先说个不少人都忽略的事实:Let’s Encrypt 这类免费证书把有效期设计成 90 天,不是拍脑袋定的,而是出于安全考虑。证书有效期越短,就算私钥意外泄露,攻击者能用它伪装你的时间窗口也越短。站在 CA 的角度,这是合理的;站在运维的角度,这就意味着每 90 天你就得把“续期”这件事重新捡起来一次。
手动续期看起来就几步:登录证书管理后台、选择域名、生成新证书、把证书文件下载下来、上传到服务器、修改或确认 Nginx 配置、reload,然后还要找个在线工具检测一下证书链是否完整。一次两次没问题,但几十个域名、每 90 天来一轮,纯靠人肉去扛,迟早要出事。我自己就经历过一次:某个客户域名的证书在我出差那几天悄悄过期,结果客户早上打开网站看到大红叉,电话直接打过来,场面一度非常尴尬。
更麻烦的是,90 天这个周期放在人的工作节奏里特别容易被忽略。第一天你会记着,第二周你可能还记得,到第七周、第八周的时候,手头事情一多,你就开始指望“日历提醒”,结果提醒弹出来那天你正好在开会,想着“下午再弄”,然后就没有然后了。所以后来我彻底想通了:这种重复性高、规则明确、出错代价又不小的事情,就应该交给程序去干,人不要参与。
1.2 为什么是acme.sh + 阿里云DNS API,而不是其他方案
市面上做证书自动化的工具不少,我实际用下来,最顺手的组合就是 acme.sh 加阿里云 DNS API。先说说为什么不是别的方案。
第一种备选是用阿里云控制台里的免费证书。这个方式对小白最友好,但有个硬伤:个人免费证书有效期也只有 3 个月(现在有些产品线已经缩短到 90 天),而且申请流程还是要登录网页去点,不支持泛域名,更关键的是没有开放的 API 让你全自动签发和部署。你依然绕不开“每隔一阵去网页上操作一次”这件事,只是把操作对象从 Let’s Encrypt 换成了阿里云控制台而已。
第二种备选是 Certbot。Certbot 功能很强,但它是 Python 生态,安装时要拉一堆依赖;DNS 插件层面,虽然官方和社区提供了很多插件,但针对阿里云 DNS 的插件配置起来不算省心。如果你只是单域名、并且 80 端口方便对外开放,Certbot 没问题;但一旦涉及泛域名、涉及国内云厂商的解析 API,体验就会打折。
第三种就是我最终用的 acme.sh。它是纯 Shell 实现的 ACME 客户端,安装就是一条 curl 命令,不依赖 Python、不依赖 Node,装完自带 cron 定时任务,每天自动检查证书状态,到期前自动续期。更重要的是,它对 DNS API 的支持非常全,阿里云、腾讯云、Cloudflare 这些常见服务商都有现成的插件。你只要配置好 AccessKey,它就能在签发前自动去阿里云解析里加一条 TXT 记录,等 CA 验证完成后再自动删掉,整个过程不需要你碰网页控制台。
这里多解释一句为什么强调“阿里云 DNS API”。ACME 协议在验证你对域名有控制权时,有两种常见方式:一种是在域名对应的 Web 服务上放一个临时文件,叫 HTTP-01;另一种是在域名的 DNS 解析记录里加一条随机 TXT 记录,叫 DNS-01。DNS-01 这条路径天然适合 API 自动化,因为加解析记录这件事可以直接通过阿里云的 OpenAPI 完成,acme.sh 只需要调用一次 API,剩下的等待和清理都是自动的。如果你的域名恰好在阿里云解析,那这套组合就是最顺的;就算你的域名在其他解析商,acme.sh 也有对应的插件,思路一模一样。
2. 动手前的原理准备:两种ACME校验方式
2.1 HTTP-01校验和DNS-01校验,我该选哪个
在正式开始操作之前,建议先花三分钟把校验原理搞清楚。ACME 协议的核心目标只有一个:证明“你拥有这个域名”。CA 不会听你空口说,它会要求你在某个地方放一个只有域名控制者才能放的东西,然后它去检查。
HTTP-01 的方式是:CA 给你一个随机 token,你要把这个 token 放到一个固定路径下,比如http://你的域名/.well-known/acme-challenge/随机token,然后 CA 通过公网访问这个 URL,看到内容一致,就认为你拥有该域名。这个方式实现起来最简单,但要求你的服务器必须能被公网通过 80 端口访问。很多场景不满足这个条件,比如家庭宽带没有公网 80 端口、服务器防火墙只放了 443、或者你压根不想为证书验证单独开启 HTTP 服务。
DNS-01 的方式是:CA 让你在域名的 DNS 解析记录里添加一条 TXT 记录,记录名通常是_acme-challenge.example.com,内容是 CA 指定的随机字符串。CA 通过查询这条 TXT 记录来判断你是否拥有域名。这种方式的好处是,不依赖 80 端口、不依赖 Web 服务器,只要你能控制域名的 DNS 解析,就能完成验证。
两种方式的对比如下:
| 对比项 | HTTP-01 | DNS-01 |
|---|---|---|
| 验证原理 | 在 Web 根目录放临时文件 | 在 DNS 添加 TXT 记录 |
| 是否需要公网 80 端口 | 需要 | 不需要 |
| 是否支持泛域名 | 不支持 | 支持 |
| 是否需要手动操作 DNS | 不需要 | 需要配合 DNS API 自动化 |
| 自动化难度 | 低 | 中,但自动化后最省心 |
我现在的服务器基本都是默认只开 443,80 端口很多情况下压根不监听,所以我几乎只用 DNS-01。再加上我的域名解析都在阿里云,--dns dns_ali这个参数就是 acme.sh 专门为阿里云 DNS 准备的插件。
2.2 为什么泛域名证书必须走DNS验证
泛域名证书,也就是证书里包含*.example.com这种通配符的证书,有张“万能通行证”的爽感。你只要签一张,以后blog.example.com、api.example.com、shop.example.com都能用,不用每个子域名单独申请,特别适合子域名多、又不想逐个维护的场景。
但泛域名证书不能通过 HTTP-01 签发,原因很简单:你没法在“所有子域名”的 Web 根目录下都放一个对应的验证文件,CA 也不知道该访问哪个具体域名。DNS-01 就没有这个问题,因为通配符属于同一套 DNS 区域,你只要在example.com这个区域的解析记录里加一条_acme-challenge.example.com的 TXT 记录,CA 验证通过,整张通配符证书就下来了。
这也是为什么“泛域名”和“DNS API 自动化”是天然的一对。acme.sh 的dns_ali插件做的事,就是把“添加 TXT 记录、等生效、请求 CA 验证、删除 TXT 记录”这一整套动作变成全自动。你只需要在命令行里把域名列表写完,剩下的交给它。
3. 阿里云侧准备:RAM子用户与最小权限
3.1 创建RAM子用户并生成AccessKey
要让 acme.sh 能自动操作你的 DNS 解析,它需要一组阿里云的 AccessKey。这里有两件事必须强调:第一,不要用主账号的 AccessKey;第二,每个子账号的权限要做到最小化。主账号的 AccessKey 权限太大,一旦泄露,别人不仅能改你的解析记录,还能动你账号里的其他资源,风险极高。正确的做法是创建一个专门的 RAM 子用户,只给它 DNS 解析相关的权限。
具体操作如下:
- 登录阿里云控制台,进入“RAM 访问控制”页面。
- 左侧菜单选择“身份管理 > 用户”,点击“创建用户”。
- 登录名称可以取
acme-dns之类的名字,访问方式一定要勾选“OpenAPI 调用访问”。 - 创建成功后,页面会显示 AccessKey ID 和 AccessKey Secret,Secret 只显示这一次,记得立刻保存到安全的地方,最好存到密码管理器里。
- 回到用户列表,找到刚创建的用户,进入“权限管理”,准备给它授权。
3.2 给AccessKey授权:建议用最小权限策略
在 RAM 控制台里,最简单的做法是给这个子用户添加系统自带的AliyunDNSFullAccess策略。这个名字听起来很大,但至少它只局限于 DNS 解析服务,不会牵扯到 ECS、OSS 等其他资源。如果你不想折腾,直接加这个系统策略就够了。
不过我更推荐再往前一步,用一个自定义的最小权限策略,只允许 acme.sh 在解析记录里做“增删查”这三件事。你可以创建一个自定义权限策略,策略内容如下:
{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": [ "alidns:AddDomainRecord", "alidns:DeleteDomainRecord", "alidns:DescribeDomainRecords" ], "Resource": "*" } ] }这段策略的意思是:只允许调用阿里云 DNS 的添加解析记录、删除解析记录、查询解析记录这三个 API。acme.sh 在签发证书和续期证书时,需要做的就是“添加 TXT 记录”和“验证通过后删除 TXT 记录”,偶尔会查询一下记录状态。给它这三个 Action 完全够用。
把这个自定义策略创建好之后,在 RAM 用户详情页点击“添加权限”,搜索刚才创建的策略名称,授权即可。授权之后,阿里云侧的准备就完成了。这里再啰嗦一句:AccessKey 是敏感信息,千万别提交到 Git 仓库,别写进会公开的配置文件里,不然等于把 DNS 的控制权拱手送人。
4. acme.sh安装、配置与首次签发实操
4.1 安装acme.sh并设置默认CA
现在开始服务器端的操作。不管你的服务器是 Ubuntu、Debian 还是 CentOS,acme.sh 的安装方式都一样。登录服务器,执行:
curl https://get.acme.sh | sh -s email=你的邮箱@example.com安装脚本跑完之后,会自动把 acme.sh 安装到当前用户的~/.acme.sh/目录下,并把~/.acme.sh/acme.sh的别名写进你的 shell 配置文件。如果你用的是 bash,执行source ~/.bashrc让别名生效,然后验证一下:
acme.sh --version看到版本号输出,说明安装成功。不同发行版可能提醒你需要安装curl或socat,按提示装上即可。
安装好之后,我建议立刻做一件事:显式设置默认 CA。acme.sh 在不同版本里默认使用的免费 CA 不完全一样,有的版本默认 Let’s Encrypt,有的版本默认 ZeroSSL,为避免后续行为不一致,直接指定:
acme.sh --set-default-ca --server letsencrypt为什么要这么干?主要原因是我希望证书申请和续期的行为可预期。Let’s Encrypt 在业内验证时间久、兼容性好,而且 acme.sh 对它的支持最成熟。指定默认 CA 之后,后面所有域名都用这套标准,省得出现“昨天签发好端端的,今天换个 CA 出幺蛾子”的情况。
4.2 配置阿里云DNS凭据并签发证书
接下来配置阿里云 DNS API 的凭据。先手动导出两个环境变量,让 acme.sh 知道该用哪组 AccessKey:
export Ali_Key="你的AccessKey ID" export Ali_Secret="你的AccessKey Secret"然后执行签发命令。以给example.com和它的所有一级子域名签发泛域名证书为例:
acme.sh --issue --dns dns_ali -d example.com -d "*.example.com"命令里几个参数拆开看:
--issue:发起证书签发请求。--dns dns_ali:使用 DNS 验证方式,并且指定使用阿里云 DNS 插件。-d example.com:把主域名加进证书。-d "*.example.com":把泛域名加进证书。
这里要注意,*.example.com并不包含example.com本身,所以为了证书既能覆盖根域名又能覆盖子域名,你需要在一条命令里同时写上example.com和*.example.com。
首次执行时,acme.sh 会调用阿里云 DNS 的 API,自动添加一条_acme-challenge.example.com的 TXT 记录。日志里会出现类似Adding TXT record的提示,随后它会等待一段时间让解析生效。这个等待是正常的,通常几十秒,不要中途按Ctrl+C把进程掐了。等 CA 验证完成,acme.sh 会自动删除那条 TXT 记录,然后开始下载新证书。
签发成功后,证书文件默认存放在~/.acme.sh/example.com_ecc/目录下。之所以带_ecc后缀,是因为 acme.sh 默认使用 ECC 密钥,也就是 ECDSA 算法生成的私钥。ECDSA 密钥更短、性能更好,现代浏览器都支持,直接用就行。
顺便说一句,第一次签发时,acme.sh 会把Ali_Key和Ali_Secret保存到~/.acme.sh/account.conf文件里,之后你再为其他域名签发证书,就不需要重复 export 了。这个文件里存的是明文密钥,务必注意权限和保管,别让它曝光。
4.3 首次签发时自动添加与删除TXT记录的过程观察
第一次跑签发命令的时候,我强烈建议你别只看结果,稍微盯一眼过程。用阿里云 RAM 子用户执行签发后,你可以打开阿里云控制台的“云解析 DNS”列表,找到example.com这个域名,刷新解析记录。你会看到一条系统自动创建的 TXT 记录,记录名是_acme-challenge,内容是一串随机字符串,类型是 TXT。
这个瞬间非常有“见证魔法”的感觉:之前你手动添加解析记录时,至少要登录控制台、找到域名、点击添加解析、填写记录类型、记录值、TTL,现在这一切都被 API 接管了。等签发流程结束,最多几分钟,你再刷新一次页面,那条 TXT 记录已经不在了。这就是 DNS API 自动化的价值:它把原本需要人工参与的“验证 + 清理”环节,变成了一个不可见但可靠的闭环。
如果你的签发日志里出现了TXT record created和Sleep 20 seconds之类的输出,都是正常节奏。如果过程中报错,比如权限不足、域名不存在、记录添加失败,别急着慌,后面的问题排查章节会专门讲。
5. 证书部署到Nginx并打通自动续期
5.1 用install-cert把证书放到Nginx的稳定路径
证书签发成功只是第一步,更关键的是把证书“安装”到一个 Nginx 能稳定读取的位置。很多教程到这里就直接让你从~/.acme.sh/里复制文件,我不建议这么做。原因有两个:第一,~/.acme.sh/下是 acme.sh 的内部存储目录,它的文件结构在版本升级时可能变化;第二,Nginx 的工作进程通常以www-data或nginx用户运行,它未必有权限读你 root 家目录下的私钥文件。
正确做法是使用 acme.sh 自带的安装导出命令:
acme.sh --install-cert -d example.com \ --key-file /etc/nginx/ssl/example.com.key \ --fullchain-file /etc/nginx/ssl/example.com.pem \ --reloadcmd "systemctl reload nginx"先执行mkdir -p /etc/nginx/ssl把目标目录创建好,再跑这条命令。这里每个参数都值得说一下:
-d example.com:指定要安装哪个域名对应的证书。--key-file:私钥文件的最终存放路径。--fullchain-file:完整证书链文件的存放路径。它包含站点证书和中间证书,Nginx 配置时用这个文件最省心。--reloadcmd:证书安装或续期成功后要自动执行的命令。这里是systemctl reload nginx,让 Nginx 重新加载证书。
--reloadcmd是一个很容易被忽略但极其重要的参数。它的作用是:以后每次 acme.sh 自动续期成功,都会自动帮你执行一次 Nginx reload,让新证书立即生效。你不需要再写额外的钩子脚本,也不用担心续期了但 Nginx 还在用旧证书的问题。
5.2 Nginx站点配置与HTTPS生效验证
证书文件就位后,修改 Nginx 站点配置。打开你的站点配置文件,比如/etc/nginx/conf.d/example.com.conf,写入以下内容:
server { listen 443 ssl http2; server_name example.com www.example.com; ssl_certificate /etc/nginx/ssl/example.com.pem; ssl_certificate_key /etc/nginx/ssl/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { root /var/www/html; index index.html; } }如果你的站点原本只有 80 端口,新增这段配置后,别忘了检查配置语法。执行:
nginx -t输出syntax is ok和test is successful之后,再执行 reload:
systemctl reload nginx到这里,HTTPS 应该已经可以访问了。用浏览器打开https://example.com,地址栏应该出现小锁图标。想从命令行确认证书信息的话,可以执行:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -subject -issuer -dates这个命令可以看到证书的签发对象、签发机构和有效期,实测下来非常方便。
5.3 验证自动续期链路是否真的能跑通
先说结论:acme.sh 安装时会在系统 crontab 里自动注册一个定时任务,每天执行一次检查。它内部有判断逻辑,只有当证书距离过期不足 30 天时,才会真正发起续期请求,所以不用担心它每天都去打扰 CA。
验证定时任务是否存在的命令:
crontab -l | grep acme正常情况下你会看到一行类似这样的输出:
13 6 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null这说明 acme.sh 的自动续期已经挂上了。为了确认整条链路真的能跑通,我建议你做一次手动强制续期测试。注意,这个操作会真实地向 CA 申请一张新证书,所以别在刚签完证书之后频繁执行,Let’s Encrypt 对同一域名签发的重复证书有数量限制,频繁强制续期可能触发限流。测试频率控制在“确认链路没问题就行,平时不跑”。
测试命令:
acme.sh --renew -d example.com --force观察输出,如果看到续期成功,再检查一下 Nginx 进程是否被 reload 过。你可以看一下 Nginx 的启动时间或直接重新访问网站确认证书日期已经更新。只要这次强制续期成功,并且 Nginx 自动 reload 生效,你的自动续期链路就正式闭环了。以后证书到期前,cron 会自动触发续期,续期成功会自动运行--reloadcmd,你再也不用管了。
6. 常见错误排查与我的避坑笔记
6.1 高频报错与解决办法速查表
用这套方案一年多,我把自己遇到过的、以及帮别人排查过的高频问题整理成了下面这张表,建议收藏备用。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
提示Ali_Key/Ali_Secret未设置 | 环境变量没导出,或者account.conf里不存在 | 重新执行export Ali_Key和Ali_Secret,再执行签发命令 |
阿里云 API 返回403 Forbidden | RAM 子用户权限不足,或授权未生效 | 检查权限策略,确认已授予AliyunDNSFullAccess或自定义 DNS 策略,一般等一两分钟生效 |
| 签发时提示域名不存在 | 域名不在当前阿里云账号的云解析列表中 | 确认域名已经在阿里云云解析中,并且已经添加了该域名的解析记录 |
| 证书签发成功但浏览器报警 | 证书链不完整,或 Nginx 只读取了站点证书而不是完整链 | 确认 Nginx 中使用的是--fullchain-file生成的文件,不要用仅包含站点证书的 crt 文件 |
| 访问 HTTPS 时证书还是旧日期 | 续期后没有触发 reload | 检查--install-cert时是否带了--reloadcmd,没有就重新执行一次安装命令 |
泛域名证书覆盖不了a.b.example.com | 通配符只匹配一级子域名 | *.example.com不覆盖a.b.example.com,需要为更深层级单独申请证书 |
| 日志里出现 CA 限流提示 | 短时间内频繁强制续期 | 停止手动--force续期,等待 CA 限制解除,日常依赖 cron 自动续期即可 |
acme.sh 的日志文件在~/.acme.sh/acme.sh.log,遇到任何看不懂的报错,第一件事就是去翻日志。日志里会把 API 调用过程、CA 返回的每一步都记录下来,排查效率会高很多。
6.2 几个我亲手踩过、也看别人踩过的坑
第一个坑:只--issue不--install-cert。我最早以为只要证书签发成功,Nginx 就能自动用上。结果发现证书文件确实更新了,但 Nginx 配置文件里指向的还是旧的证书路径,更坑的是我根本不知道它已经更新了。后来才明白,--issue只是负责“签发证书”,--install-cert才是负责“把证书文件复制到指定位置,并执行 reload 命令”。这两步缺一不可,只做第一步的话,你的自动续期只是“自动申请”,不是“自动部署”。
第二个坑:把私钥文件放在家目录里,Nginx 读不了。有一次换服务器,我图省事,直接用~/.acme.sh/*.key路径写进 Nginx 配置,结果 Nginx reload 时一直报权限错误。后来进了/etc/nginx/ssl目录,还要注意目录权限。Nginx 主进程通常是 root 启动,但 worker 进程会降权到nginx或www-data用户,私钥文件的权限建议设置为640,属组设置为 Nginx 运行用户,比如:
chown -R root:nginx /etc/nginx/ssl chmod 640 /etc/nginx/ssl/*这样既保证 Nginx 能读取,又避免其他无关用户拿到私钥内容。
第三个坑:泛域名证书的“泛”的范围理解偏差。*.example.com确实能覆盖www.example.com、api.example.com、blog.example.com,但它覆盖不了dev.api.example.com。如果你有这种多级子域名的需求,老老实实把多级域名作为一个独立域名去签,或者评估一下是不是真的需要那么深的子域名层级。
第四个坑:把密钥当成普通配置乱放。acme.sh 会把阿里云 AccessKey 明文存在~/.acme.sh/account.conf里,这本身没问题,但如果你习惯把这台服务器的家目录打包备份到公开仓库、或者截图发到群里求助,那密钥就泄了。阿里云控制台里一旦发现 AccessKey 泄露,第一时间去 RAM 里删掉并重新生成,别犹豫。
6.3 多域名环境下的批量签发小技巧
如果你跟我一样,手里不止一个域名,一条条执行--issue会有点笨。这里分享一个小技巧:把域名列表写进一个数组,用 for 循环批量签发和安装。比如:
domains=( "example.com" "blog.example.com" "another.com" ) for domain in "${domains[@]}"; do acme.sh --issue --dns dns_ali -d "$domain" --server letsencrypt acme.sh --install-cert -d "$domain" \ --key-file "/etc/nginx/ssl/${domain}.key" \ --fullchain-file "/etc/nginx/ssl/${domain}.pem" \ --reloadcmd "systemctl reload nginx" done这个脚本只适合处理普通域名,泛域名需要单独写成-d example.com -d "*.example.com",所以不要完全无脑套用。但它能很直观地说明一件事:一旦走通自动化,加域名这件事就从“繁琐操作”变成了“往数组里加一行字符串”。
这套流程我用到现在已经快两年了。坦白说,中间最值钱的经验不是某条具体命令,而是把“人”从循环里拿掉——证书续期这种事,人的记性是最不可靠的组件。最后再提一个建议:如果你机器上还有别的域名,别一个个手敲,把上面的 for 循环脚本改一改,新增一个站点整个流程不超过一分钟。这才是这套方案真正让人上瘾的地方。