外观
SSL 证书有哪几种
先说名字,现在说的 SSL 证书,实际上指的是 TLS 证书。SSL 是 TLS 的前身,它的 2.0 和 3.0 都因为安全漏洞早已废弃,现在的加密通信一律走 TLS。但废弃的是协议,不是证书——证书格式这些年没变过,同一张证书在两种协议下都成立,所以「SSL 证书」这个叫法就出于历史习惯保留了下来。
平时听到的证书名目不少,其实分属三个维度,任何一张证书都同时带着这三个属性:验证等级、覆盖范围、密钥算法。
按验证等级:DV、OV、EV
验证等级的区别只在于 CA 签发前核实了你什么。
| 等级 | CA 核实什么 | 签发耗时 | 费用 |
|---|---|---|---|
| DV(域名验证) | 你能控制这个域名 | 全自动,几分钟 | 免费、付费都有 |
| OV(组织验证) | 申请方是真实存在的组织,要营业执照等材料 | 人工介入,几天 | 按年付费 |
| EV(扩展验证) | 对组织做更严格的实体审查 | 人工介入,更久 | 按年付费,最贵 |
DV 的验证方式是让你在 DNS 里加一条指定记录,或者在网站根目录放一个指定文件,以此证明你控制得了这个域名。OV 和 EV 多出来的是对「你是谁」的核实,这部分只能人工做,所以快不起来。
EV 过去有个看得见的好处:地址栏会显示公司名,一度还带绿色底色。这个展示在 2019 年前后被 Chrome 和 Firefox 取消了,现在三种证书在地址栏的样子完全一致,EV 的企业名要点开证书详情才看得到。
常有人以为 EV 更贵是因为加密更强。其实数据用什么算法加密、加密强度多大,是服务器上配的 TLS 版本和密码套件说了算,证书等级不参与这件事,它只决定证书里写了哪些身份信息。所以同一台机器,哪怕把 DV 换成 EV,算法和强度都不会变,多花的钱买到的是证书里多出来的几行组织信息。
按覆盖范围:单域名、多域名、泛域名
证书里写着它对哪些主机名有效,客户端会逐字比对,对不上就报错。
- 单域名只保护一个主机名,而且是精确匹配。
example.com的证书用在www.example.com上会报错,证书不会因为这两个名字同属一个域就放行——它们是两个不同的主机名。 - 多域名(也叫 SAN 证书)在一张证书里列多个主机名,彼此不必相关。Let's Encrypt 允许单张证书最多放 100 个名字。
- 泛域名用
*.example.com覆盖全部一级子域名。它不包含主域名本身,也不递归——比如a.b.example.com就不在范围内。
最常见的做法是后两种合用:一张证书的名字列表里同时写 example.com 和 *.example.com,主域名和所有一级子域名一次覆盖。
很多人觉得
www.example.com就是example.com,这个印象来自实践:站点一般把两个名字一起配上——DNS 各给一条记录、证书里两个都写、nginx 的server_name两个都列,再用 301 把一个跳到另一个。但它们始终是两个主机名,www只是example.com的一个子域名,上面任何一步漏掉谁,那个名字就访问不了。
按密钥算法:RSA、ECC
现在主流的密钥算法有两种:
RSA 的安全性来自大整数分解的难度。它在 1977 年就被提出,是用得最久的一种密钥算法。现在的密钥长度起步 2048 位,往上还有 3072 和 4096。
ECC 的安全性来自椭圆曲线上的离散对数难度。它出现得晚,证书里对应的签名算法叫 ECDSA,常用曲线是 P-256 和 P-384。
两者的差距主要在效率。按 NIST 的安全强度对应,256 位的 ECC 密钥和 3072 位的 RSA 密钥属于同一档,长度差了十几倍。密钥短带来的好处是证书文件更小、握手时服务器做签名的开销更低。
目前的主流算法是 ECC。RSA 如今剩下的用处是兼容很老的客户端,比如 Windows XP 那个年代的系统和早期 Android。
用 acme.sh 自动签发
OV 和 EV 要人工核实组织身份,这部分成本压不下来,所以没有免费版。DV 分免费和付费两种,付费版过去能签 1 年甚至更长,免费版就短得多,一般 90 天左右。到期前需要人工重新申请、部署一遍,低效而且容易忘。
但有一个好消息是,很多免费 CA 提供了自动化入口,通过 ACME 协议将签发、域名验证、续期全部交给程序代办,不用登网页填表,比如 Let's Encrypt、ZeroSSL。
客户端这边主流是 acme.sh 和 certbot。acme.sh 是一个 POSIX shell 脚本,不依赖 Python 运行时,装完就一个目录,而且自带的 DNS 服务商插件覆盖极广,包括阿里云在内——签发泛域名要靠这个。
签发之前,CA 得先确认这个域名归你管。验证方式有两种:往网站根目录放一个指定文件等 CA 来读(HTTP-01),或者在域名下加一条指定内容的 TXT 记录等 CA 去查(DNS-01)。泛域名只能走 DNS-01,而且这条路不依赖 80 端口、不用碰 web 服务,签发失败也影响不到正在跑的站点。代价是得让 acme.sh 能自动增删那条 TXT 记录,也就是给它一副能操作 DNS 的凭证。
我个人选择的组合是:证书来自 Let's Encrypt,签发由 acme.sh 完成,域名验证走阿里云 DNS。
换成 ZeroSSL 或者别家 DNS 服务商,流程是一样的,差别只在几个参数。
准备阿里云 AccessKey
acme.sh 要自动增删那条 TXT 记录,就得有一副能调阿里云 DNS 接口的 AccessKey。这副密钥会长期留在服务器上,所以不能图省事用主账号的——主账号 AccessKey 等于整个阿里云账号的控制权,服务器、数据库、对象存储、账单都在里面。阿里云控制台自己也在创建页面上提示:AccessKey 泄露会威胁该账号下所有资源的安全。
正确做法是单独建一个 RAM 用户,只给它管 DNS 的权限。
- 阿里云控制台 → RAM 访问控制 → 用户 → 创建用户

要填的是登录名称和显示名称,访问配置只勾【使用永久 AccessKey 访问】。这个用户只给程序调接口用,不需要登控制台,所以【使用控制台访问】别勾,省掉一条攻击面。再勾上【我确认必须创建 AccessKey】,点【确定】。
成功后会跳转至创建结果页面。

点击复制按钮,保留相关信息
text
登录名称 dns@******.onaliyun.com
登录密码
AccessKey ID LTA***wr8
AccessKey Secret CtQ***Qkn
SecurityPhoneDevice
SecurityEmailDevice注意
一定要点击复制并保存好 AccessKey ID 和 AccessKey Secret,页面关闭后没有任何方式再次查询 AccessKey Secret。
- 阿里云控制台 → RAM 访问控制 → 授权 → 新增授权

授权主体选择刚刚创建的用户,权限策略搜索并勾选 AliyunDNSFullAccess,点击【确认新增授权】按钮。
注意
只给 AliyunDNSFullAccess 是把风险压到相对最低,但不等于没有风险:这副密钥泄露,对方仍然能改你账号下所有域名的解析,也就是域名劫持。所以 AccessKey Secret 要当密码一样保管。
DNS-PERSIST-01
有个新方案值得留意:在 _validation-persist.<域名> 放一条标明 CA 和 ACME 账户的 TXT 记录,设一次就不用再动,后续签发和续期都复用它,服务器上从此不需要 DNS 写权限。
代价是授权从一次性变成长期有效,ACME 账户密钥一旦泄露,后果比现在持续得多,官方公告把这个权衡讲得很清楚。这把密钥现在也躺在服务器上,位置是 ~/.acme.sh/ca/acme-v02.api.letsencrypt.org/directory/account.key。
acme.sh 3.1.4 已经支持 --dns-persist-days,但 Let's Encrypt 目前只在 staging 环境提供,生产环境原计划 2026 年第二季度上线,到 9 月仍未开放。
安装 acme.sh
所有命令都在服务器宿主机上执行。示例域名用 example.com,路径和容器名请换成你自己的。
bash
curl https://get.acme.sh | sh -s email=你的邮箱text
[Mon Sep 21 09:29:20 AM CST 2026] Installing from online archive.
[Mon Sep 21 09:29:20 AM CST 2026] Downloading https://github.com/acmesh-official/acme.sh/archive/master.tar.gz
[Mon Sep 21 09:29:22 AM CST 2026] Extracting master.tar.gz
[Mon Sep 21 09:29:22 AM CST 2026] Installing to /root/.acme.sh
[Mon Sep 21 09:29:22 AM CST 2026] Installed to /root/.acme.sh/acme.sh
[Mon Sep 21 09:29:22 AM CST 2026] Installing alias to '/root/.bashrc'
[Mon Sep 21 09:29:22 AM CST 2026] Close and reopen your terminal to start using acme.sh
[Mon Sep 21 09:29:22 AM CST 2026] Installing alias to '/root/.cshrc'
[Mon Sep 21 09:29:22 AM CST 2026] Installing alias to '/root/.tcshrc'
[Mon Sep 21 09:29:22 AM CST 2026] Installing cron job
[Mon Sep 21 09:29:22 AM CST 2026] bash has been found. Changing the shebang to use bash as preferred.
[Mon Sep 21 09:29:25 AM CST 2026] OK
[Mon Sep 21 09:29:25 AM CST 2026] Install success!邮箱会成为 ACME 账户的绑定邮箱,填一个能收到信的。输出末尾会提到它装到了 ~/.acme.sh/、往 shell 配置里加了 alias、并且装了一个 cron job——这三件事后面都会用到。
输出里那句 Close and reopen your terminal 是让你重开终端,好让 alias 生效。不想重开就 source 一下:
bash
source ~/.bashrc确认版本:
bash
acme.sh --versiontext
https://github.com/acmesh-official/acme.sh
v3.1.6版本号要留意。低于 3.1.4 的先升一下(acme.sh --upgrade),3.1.4 改过续期逻辑,旧版本在证书有效期缩短后有过期风险。
签发证书
bash
# 只需要导一次。acme.sh 调用阿里云 DNS 插件时就会把这两个值写进 `~/.acme.sh/account.conf`,之后续期自动读取。
export Ali_Key="你的 AccessKey ID"
export Ali_Secret="你的 AccessKey Secret"
acme.sh --issue --dns dns_ali \
-d example.com -d '*.example.com' \
--server letsencrypttext
[Mon Sep 21 09:33:16 AM CST 2026] Using CA: https://acme-v02.api.letsencrypt.org/directory
[Mon Sep 21 09:33:16 AM CST 2026] Account key creation OK.
[Mon Sep 21 09:33:16 AM CST 2026] Registering account: https://acme-v02.api.letsencrypt.org/directory
[Mon Sep 21 09:33:18 AM CST 2026] Registered
[Mon Sep 21 09:33:18 AM CST 2026] ACCOUNT_THUMBPRINT='**'
[Mon Sep 21 09:33:18 AM CST 2026] Creating domain key
[Mon Sep 21 09:33:18 AM CST 2026] The domain key is here: /root/.acme.sh/example.com_ecc/example.com.key
[Mon Sep 21 09:33:18 AM CST 2026] Multi domain='DNS:example.com,DNS:.example.com'
[Mon Sep 21 09:33:21 AM CST 2026] Getting webroot for domain='example.com'
[Mon Sep 21 09:33:21 AM CST 2026] Getting webroot for domain='*.example.com'
[Mon Sep 21 09:33:21 AM CST 2026] Adding TXT value: H3BuTi_6i-tiihctwPkn5AnJGzMIOc8sqmbsRnZB7A8 for domain: _acme-challenge.example.com
[Mon Sep 21 09:33:23 AM CST 2026] The TXT record has been successfully added.
[Mon Sep 21 09:33:23 AM CST 2026] Adding TXT value: W5QEhc5XslHagetNMRzaTY_3yC0e1noKj5OFmP6IRhk for domain: _acme-challenge.example.com
[Mon Sep 21 09:33:24 AM CST 2026] The TXT record has been successfully added.
[Mon Sep 21 09:33:24 AM CST 2026] Let's check each DNS record now. Sleeping for 20 seconds first.
[Mon Sep 21 09:33:46 AM CST 2026] You can use '--dnssleep' to disable public dns checks.
[Mon Sep 21 09:33:46 AM CST 2026] See: https://github.com/acmesh-official/acme.sh/wiki/dnscheck
[Mon Sep 21 09:33:46 AM CST 2026] Checking example.com for _acme-challenge.example.com
[Mon Sep 21 09:33:46 AM CST 2026] Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: 35
[Mon Sep 21 09:33:56 AM CST 2026] Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: 28
[Mon Sep 21 09:33:56 AM CST 2026] Success for domain example.com '_acme-challenge.example.com'.
[Mon Sep 21 09:33:56 AM CST 2026] Checking example.com for _acme-challenge.example.com
[Mon Sep 21 09:33:56 AM CST 2026] Success for domain example.com '_acme-challenge.example.com'.
[Mon Sep 21 09:33:56 AM CST 2026] All checks succeeded
[Mon Sep 21 09:33:56 AM CST 2026] Verifying: example.com
[Mon Sep 21 09:33:57 AM CST 2026] Pending. The CA is processing your order, please wait. (1/30)
[Mon Sep 21 09:34:01 AM CST 2026] Success
[Mon Sep 21 09:34:01 AM CST 2026] Verifying: *.example.com
[Mon Sep 21 09:34:02 AM CST 2026] Pending. The CA is processing your order, please wait. (1/30)
[Mon Sep 21 09:34:06 AM CST 2026] Success
[Mon Sep 21 09:34:06 AM CST 2026] Removing DNS records.
[Mon Sep 21 09:34:06 AM CST 2026] Removing txt: H3BuTi_6i-tiihctwPkn5AnJGzMIOc8sqmbsRnZB7A8 for domain: _acme-challenge.example.com
[Mon Sep 21 09:34:07 AM CST 2026] Successfully removed
[Mon Sep 21 09:34:08 AM CST 2026] Removing txt: W5QEhc5XslHagetNMRzaTY_3yC0e1noKj5OFmP6IRhk for domain: _acme-challenge.example.com
[Mon Sep 21 09:34:09 AM CST 2026] Successfully removed
[Mon Sep 21 09:34:09 AM CST 2026] Verification finished, beginning signing.
[Mon Sep 21 09:34:09 AM CST 2026] Let's finalize the order.
[Mon Sep 21 09:34:09 AM CST 2026] Le_OrderFinalize='https://acme-v02.api.letsencrypt.org/acme/finalize/<账户ID>/<订单ID>'
[Mon Sep 21 09:34:10 AM CST 2026] Downloading cert.
[Mon Sep 21 09:34:10 AM CST 2026] Le_LinkCert='https://acme-v02.api.letsencrypt.org/acme/cert/<证书序列号>'
[Mon Sep 21 09:34:11 AM CST 2026] Cert success.
-----BEGIN CERTIFICATE-----
(证书内容,略)
-----END CERTIFICATE-----
[Mon Sep 21 09:34:11 AM CST 2026] Your cert is in: /root/.acme.sh/example.com_ecc/example.com.cer
[Mon Sep 21 09:34:11 AM CST 2026] Your cert key is in: /root/.acme.sh/example.com_ecc/example.com.key
[Mon Sep 21 09:34:11 AM CST 2026] The intermediate CA cert is in: /root/.acme.sh/example.com_ecc/ca.cer
[Mon Sep 21 09:34:11 AM CST 2026] And the full-chain cert is in: /root/.acme.sh/example.com_ecc/fullchain.cer
[Mon Sep 21 09:34:12 AM CST 2026] ARI suggestedWindow: 2026-11-19T03:19:44Z to 2026-11-20T22:30:33Z
[Mon Sep 21 09:34:12 AM CST 2026] Next renewal time picked from ARI window: 2026-11-20T11:10:50Z输出里有两处容易误会。error code: 35 和 28 不是签发失败,是 acme.sh 在公共 DNS 上查那条 TXT 记录时撞上了握手失败和超时,它自己会重试,紧接着两行就是 Success。末尾的 ARI suggestedWindow 和 Next renewal time 是 CA 给出的建议续期窗口以及 acme.sh 从窗口里挑定的时间点——这张证书 11 月 20 日就会自动续期。
--dns dns_ali 用阿里云 DNS 插件做 DNS-01 验证,读的就是上面 export 的那两个变量。凭证只需要导一次,插件会把它们写进 ~/.acme.sh/account.conf,以后续期自动读取。
-d '*.example.com' 是泛域名。因为它不覆盖主域名本身,两个 -d 都得写。单引号不能省,否则 shell 会把 * 当通配符展开。
--server letsencrypt 这个参数别漏。acme.sh 的默认 CA 是 ZeroSSL——3.1.6 的源码里 DEFAULT_CA 仍然指向它——不写这个参数,签出来的就是 ZeroSSL 的证书。输出第一行会打印 Using CA:,可以核对一下自己签到了哪家。
签发通常 1~2 分钟:调 API 加 TXT 记录,等解析生效,让 CA 校验,然后把记录清掉。卡在 DNS 验证阶段的话,常见原因是三个:凭证没导入或子用户权限没给对(日志里是 API 返回的权限错误)、域名的 DNS 不在阿里云托管(dns_ali 只能操作阿里云 DNS 里的域名)、域名刚加到阿里云解析还没生效。
调试阶段先用 --server letsencrypt_test 走 staging 环境。它签出来的证书浏览器不认,但不消耗正式环境额度。正式环境的限制是每个注册域名每 7 天最多 50 张证书、每账号每 3 小时 300 个新订单(Rate Limits),反复试错很容易撞上,撞上之后只能等额度慢慢回填。
把证书装到 nginx 读得到的地方
签发出来的文件在 ~/.acme.sh/example.com_ecc/ 下。目录名带 _ecc 后缀是因为 acme.sh 默认签 ECC 证书,后面几条命令要加 --ecc 也是这个原因——不加的话它会去找同名的 RSA 证书,然后报找不到。
这个目录里的文件不要在 nginx 里直接引用,acme.sh 的 README 明确写了它仅供内部使用、结构以后可能变。要用 --install-cert 复制出去:
bash
acme.sh --install-cert -d example.com --ecc \
--key-file /srv/www/ssl/example.com.key \
--fullchain-file /srv/www/ssl/example.com.pem \
--reloadcmd "docker exec nginx-docs nginx -s reload"text
[Mon Sep 21 09:39:14 AM CST 2026] The domain 'example.com' seems to already have an ECC cert, let's use it.
[Mon Sep 21 09:39:14 AM CST 2026] Installing key to: /srv/www/ssl/example.com.key
[Mon Sep 21 09:39:14 AM CST 2026] Installing full chain to: /srv/www/ssl/example.com.pem
[Mon Sep 21 09:39:14 AM CST 2026] Running reload cmd: docker exec nginx-docs nginx -s reload
2026/09/21 01:39:14 [notice] 30#30: signal process started
[Mon Sep 21 09:39:14 AM CST 2026] Reload successful路径按实际情况改。nginx 直接装在宿主机上的话,--reloadcmd 写 "systemctl reload nginx" 或 "nginx -s reload"。
三个参数分别指定:--key-file 私钥放哪,--fullchain-file 证书放哪,--reloadcmd 续期成功后执行什么命令。
--reloadcmd 最容易漏,而它恰好是整套自动化的关键一环。acme.sh 每次续期成功都会执行它,少了这条,续期时证书文件会更新,nginx 却仍在内存里用着旧的,直到进程重启为止。表现出来就是磁盘上的证书是新的、线上服务的证书过期了,排查起来很费时间。
nginx 跑在容器里的话,证书目录要挂进去,只读就够。compose 文件放在 /srv/www/ 下,./ssl 就是上面写入证书的 /srv/www/ssl,容器里看到的是 /etc/nginx/ssl:
yaml
services:
web:
image: nginx:alpine
volumes:
- ./ssl:/etc/nginx/ssl:ro
ports:
- '80:80'
- '443:443'不用 Docker 的话
三处不一样,其余步骤完全相同:
--reloadcmd写"systemctl reload nginx"- 上面这段挂载配置不需要
- 证书路径只有一个。
--install-cert写到哪里,nginx 配置里就填哪里,不存在宿主机和容器内两套路径——下面配置示例里的/etc/nginx/ssl/换成你自己的那个目录
nginx 配置
nginx
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
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_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=300" always;
root /usr/share/nginx/html;
index index.html;
}第一个 server 块把 80 端口的请求全部 301 到 HTTPS。80 端口的监听要留着——不监听的话,访客输入不带协议的域名时得到的是连接被拒,而不是跳转。
http2 on; 这条指令从 nginx 1.25.1 才开始有。更早的版本要写成 listen 443 ssl http2;,而这种旧写法在新版本里会告警。先用 nginx -v 确认版本再选。
ssl_protocols 只留 TLSv1.2 和 TLSv1.3。加密套件(ssl_ciphers)没写,nginx 和 OpenSSL 的默认值对个人站足够。
Strict-Transport-Security(HSTS)告诉浏览器这个域名以后一律用 HTTPS,在有效期内浏览器会自己把 http:// 改写成 https://,连那次 301 都省掉。这个头有点危险:一旦发出去,max-age 期间浏览器不接受这个域名的 HTTP 访问,也不让用户点过证书错误警告。所以示例里只写了 300 秒,跑几天确认 HTTPS 稳定了再加到一年(31536000)。拿不准的话先完全不加也没问题。要加 includeSubDomains 或者提交 HSTS preload 之前,务必确认所有子域名都支持 HTTPS,否则会把它们一起锁死,而 preload 摘除要等浏览器版本更新。
改完先测语法再重载:
bash
docker exec nginx-docs nginx -t
docker exec nginx-docs nginx -s reloadnginx -t 通过再 reload。直接 reload 一份有语法错误的配置,nginx 会拒绝加载新配置、继续用旧的跑,服务不会挂,但你会以为改动生效了。
自动续期实际是怎么跑的
网上多数 acme.sh 教程描述的续期行为是几年前那个版本的,和现在对不上。以 3.1.6 为准,实际逻辑是这样。
安装时写进 crontab 的那条任务,现在的频率是每 6 小时一次(随机分钟和起始小时,然后 +6、+12、+18),不是过去的每天一次:
bash
crontab -l | grep acmetext
22 5,11,17,23 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null任务跑起来后 acme.sh 遍历它管理的每张证书,判断是不是该续了。判断依据从 3.1.4(2026 年 7 月)起变过:
- 默认启用 ARI(ACME Renewal Information,RFC 9773)。CA 给出一个建议续期窗口,acme.sh 直接采纳,并在窗口内取一个随机点,避免全网客户端同时涌上去。cron 改成 6 小时一次就是为了及时响应这个窗口。
- 关掉 ARI(
NO_ARI=1)或者 CA 不提供 ARI 时,退回自己算:签发后第 29 天续期(DEFAULT_RENEW=30,实际是 30 天减 1 天),并且有个上限保护,续期时间永不晚于到期前 1 天。 - 用
--days显式指定过天数的话你的设定优先,但 CA 给出的窗口若比你设的更早,会采用更早的那个(3.1.5 起的行为)。
具体这张证书下次什么时候续、reloadcmd 登记成了什么,--info 能查到:
bash
acme.sh --info -d example.com --ecctext
DOMAIN_CONF=/root/.acme.sh/example.com_ecc/example.com.conf
Le_Domain=example.com
Le_Alt=*.example.com
Le_Webroot=dns_ali
Le_PreHook=
Le_PostHook=
Le_RenewHook=
Le_API=https://acme-v02.api.letsencrypt.org/directory
Le_Keylength=ec-256
Le_OrderFinalize=https://acme-v02.api.letsencrypt.org/acme/finalize/<账户ID>/<订单ID>
Le_LinkOrder=https://acme-v02.api.letsencrypt.org/acme/order/<账户ID>/<订单ID>
Le_LinkCert=https://acme-v02.api.letsencrypt.org/acme/cert/<证书序列号>
Le_CertCreateTime=1789954451
Le_CertCreateTimeStr=2026-09-21T01:34:11Z
Le_NextRenewTimeStr=2026-11-20T11:10:50Z
Le_NextRenewTime=1795173050
Le_RealCertPath=
Le_RealCACertPath=
Le_RealKeyPath=/srv/www/ssl/example.com.key
Le_ReloadCmd=docker exec nginx-docs nginx -s reload
Le_RealFullChainPath=/srv/www/ssl/example.com.pem
Le_InstallCertSuccessTime=1789954754
Le_InstallCertSuccessTimeStr=2026-09-21T01:39:14Z续期成功后 acme.sh 自动执行 --install-cert 时登记的 --reloadcmd。整条链是 cron → 检查 → 续签 → 复制文件 → 重载 nginx,全程不用人管。
回到版本这件事,它有个现在就该处理的风险。3.1.4 之前的 acme.sh 默认签发后 60 天续期,在 90 天证书上留了 30 天余量,够用。但 Let's Encrypt 已经公布了缩短有效期的时间表:2027 年 2 月 10 日起默认证书变成 64 天,2028 年 2 月再降到 45 天(Decreasing Certificate Lifetimes to 45 Days)。64 天的证书配 60 天续期,余量只剩 4 天,一次 DNS 故障或 API 报错就可能让证书过期。这份公告里专门点了这件事:按 60 天硬编码间隔续期,以后不再够用。
低于 3.1.4 就升一下,顺便看看 release notes 里有没有影响你的变更:
bash
acme.sh --upgradeacme.sh 也支持开自动升级(--upgrade --auto-upgrade)。开不开看偏好。它是个会动线上证书的脚本,自动跟进上游变更也是有风险的,定期看看 changelog 更稳妥一些。
验证与排障
配完先确认线上真的在用新证书:
bash
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -issuer -subjecttext
notBefore=Sep 21 00:35:40 2026 GMT
notAfter=Dec 20 00:35:39 2026 GMT
issuer= /C=US/O=Let's Encrypt/CN=YE1
subject= /CN=example.com-servername 不能省,同一个 IP 上有多个站点时,不带 SNI 拿到的可能是别人的证书。
跳转和响应:
bash
curl -I https://example.com
curl -I http://example.comtext
HTTP/2 200
server: nginx
date: Tue, 22 Sep 2026 08:49:53 GMT
content-type: text/html
content-length: 10140
last-modified: Mon, 21 Sep 2026 07:38:34 GMT
vary: Accept-Encoding
etag: "6ab0defa-279c"
strict-transport-security: max-age=300
x-content-type-options: nosniff
accept-ranges: bytes
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Tue, 22 Sep 2026 08:49:54 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Location: https://example.com/acme.sh 管了哪些证书、什么时候到期:
bash
acme.sh --listtext
Main_Domain KeyLength SAN_Domains Profile CA Created Renew
example.com "ec-256" *.example.com LetsEncrypt.org 2026-09-21T01:34:11Z 2026-11-20T11:10:50Z第一次配好之后,值得强制续一次,把整条链路跑通:
bash
acme.sh --renew -d example.com --force --ecctext
[Tue Sep 22 04:51:23 PM CST 2026] Renew: example.com
[Tue Sep 22 04:51:23 PM CST 2026] Renewing: 'example.com'
[Tue Sep 22 04:51:23 PM CST 2026] Renewing using Le_API=https://acme-v02.api.letsencrypt.org/directory
[Tue Sep 22 04:51:24 PM CST 2026] Using CA: https://acme-v02.api.letsencrypt.org/directory
[Tue Sep 22 04:51:24 PM CST 2026] Multi domain='DNS:example.com,DNS:*.example.com'
[Tue Sep 22 04:51:28 PM CST 2026] Getting webroot for domain='example.com'
[Tue Sep 22 04:51:28 PM CST 2026] Getting webroot for domain='*.example.com'
[Tue Sep 22 04:51:29 PM CST 2026] example.com is already verified, skipping dns-01.
[Tue Sep 22 04:51:29 PM CST 2026] *.example.com is already verified, skipping dns-01.
[Tue Sep 22 04:51:29 PM CST 2026] Verification finished, beginning signing.
[Tue Sep 22 04:51:29 PM CST 2026] Let's finalize the order.
[Tue Sep 22 04:51:29 PM CST 2026] Le_OrderFinalize='https://acme-v02.api.letsencrypt.org/acme/finalize/<账户ID>/<新订单ID>'
[Tue Sep 22 04:51:31 PM CST 2026] Downloading cert.
[Tue Sep 22 04:51:31 PM CST 2026] Le_LinkCert='https://acme-v02.api.letsencrypt.org/acme/cert/<新证书序列号>'
[Tue Sep 22 04:51:32 PM CST 2026] Cert success.
-----BEGIN CERTIFICATE-----
(证书内容,略)
-----END CERTIFICATE-----
[Tue Sep 22 04:51:32 PM CST 2026] Your cert is in: /root/.acme.sh/example.com_ecc/example.com.cer
[Tue Sep 22 04:51:32 PM CST 2026] Your cert key is in: /root/.acme.sh/example.com_ecc/example.com.key
[Tue Sep 22 04:51:32 PM CST 2026] The intermediate CA cert is in: /root/.acme.sh/example.com_ecc/ca.cer
[Tue Sep 22 04:51:32 PM CST 2026] And the full-chain cert is in: /root/.acme.sh/example.com_ecc/fullchain.cer
[Tue Sep 22 04:51:34 PM CST 2026] ARI suggestedWindow: 2026-11-20T10:37:04Z to 2026-11-22T05:47:54Z
[Tue Sep 22 04:51:34 PM CST 2026] Next renewal time picked from ARI window: 2026-11-21T03:22:48Z
[Tue Sep 22 04:51:34 PM CST 2026] Installing key to: /srv/www/ssl/example.com.key
[Tue Sep 22 04:51:34 PM CST 2026] Installing full chain to: /srv/www/ssl/example.com.pem
[Tue Sep 22 04:51:34 PM CST 2026] Running reload cmd: docker exec nginx-docs nginx -s reload
2026/09/22 08:51:34 [notice] 38#38: signal process started
[Tue Sep 22 04:51:34 PM CST 2026] Reload successful这会真向 CA 要一张新证书,占额度,别反复做。但它是唯一能提前确认「续期之后 nginx 真的会用上新证书」的办法——不跑这一次,reloadcmd 写错了你要等三个月才知道。
常见问题:
- 签发卡在 DNS 验证:凭证没导入或子用户权限不足;域名 DNS 不在阿里云;解析还没生效。
- 浏览器正常、部分客户端报证书链不完整:用了
--cert-file而不是--fullchain-file。 - 续期后线上证书没变:
reloadcmd不对,容器名改过或命令路径不对。用--force续一次立刻能复现。 - 撞上速率限制:每注册域名每 7 天 50 张,每账号每 3 小时 300 个订单。调试用
--server letsencrypt_test。 - 命令找不到:
acme.sh是装 alias 时写进 shell 配置的,新开的 shell 没 source 过就会找不到,用全路径~/.acme.sh/acme.sh最稳。 - 签发和续期日志:
~/.acme.sh/example.com_ecc/example.com.log。
国内服务器还有一个前提
证书配好了,站点仍然可能访问不了。服务器在阿里云中国内地节点的话,还有一件和证书无关但缺了就跑不起来的事:域名要完成 ICP 备案。没有备案号,域名访问会被拦住,证书配得再漂亮也没用。备案在阿里云的 ICP 代备案系统提交,个人主体通常要几个工作日到几周。这是合规要求,和用哪个端口无关。
香港或海外节点不需要备案,代价是访问延迟和链路稳定性另算,那是一套独立的取舍。
小结
配一次之后基本不用再管。命令都能查到,值得记住的是这几个判断:
- DNS-01 验证换来泛域名和与 web 服务解耦,代价是一副 DNS API 密钥。用 RAM 子用户、只给 DNS 权限,把泄露的后果限制在可收拾的范围内。
--server letsencrypt要显式写,acme.sh 默认不走 Let's Encrypt。--reloadcmd必须配,而且要强制续期验证过一次。漏了它,续期后磁盘上是新证书、线上服务还在用旧的。- 证书有效期在逐级缩短,acme.sh 的续期策略也跟着改过。别照着几年前的教程配,也别让服务器上的 acme.sh 停在旧版本。
每年花十分钟看一眼版本、证书有效期和续期日志,其余时间它自己会跑。
