手里吃灰的域名越来越多,偶尔还是会拿来测试测试,每次手动配置证书就很麻烦,刚好最近 Let’s Encrypt Authority 也由 X3 更新为了 R3 版本,使用了新的 OCSP 地址,解决了污染问题。因此又用回 acme.sh 自动签发更新证书,官方有很详细的使用文档,这里只做简单记录。
安装
curl https://get.acme.sh | sh
若后面出现 command not found
,则需要手动执行以下命令:
source ~/.bashrc
生成证书
HTTP 认证方式
该方式 acme.sh 会在你的指定的网站根目录下自动生成一个文件,来验证域名所有权,然后自动完成验证并签发证书:
acme.sh --issue -d domain.tld -d www.domain.tld --webroot /home/wwwroot/domain.tld/
完成后会自动删除验证文件,无需多余操作。
DNS 认证方式(推荐)
这里为了方便,选择通过 DNS 认证方式,这种方式不需要服务器和公网 IP,只需要 DNS 的解析记录即可完成验证,一般主流域名服务商都提供 API 接口,acme.sh 目前支持包括主流的 CloudFlare、DNSPod、Aliyun、Amazon Route53 在内的多达 131 个的域名 API,你这可以在这里查看到详细的支持列表。我的 DNS 服务器一般用的是 Cloudflare,因此这里以 Cloudflare 的 API 为例验证,获取 Cloudflare API,可以自己选择全局 API 还是单域 API,我这里选择了单域 API,获取到 API 后注意保存,然后导入:
export CF_Token="xxxxxxxxxxxxxxxxxxxxxxxx"
export CF_Account_ID="xxxxxxxxxxxxxxxxxxxxxxxx"
export CF_Zone_ID="xxxxxxxxxxxxxxxxxxxxxxxx"
其中 CF_Token 为你获取到的API密钥,CF_Account_ID 和 CF_Zone_ID 可以在你域名概述页面的 API 分栏看到。
签发证书:
acme.sh --issue --dns dns_cf -d domain.tld -d www.domain.tld
若要生成通配符证书,则:
acme.sh --issue --dns dns_cf -d domain.tld -d *.domain.tld
如果要签发 ECC 证书,需要在后面加上 --keylength
选项,下面的示例是签发一张 256 位长度的 EEC 通配符证书:
acme.sh --issue --dns dns_cf -d domain.tld -d *.domain.tld --keylength ec-256
2021 年 6 月 29 日更新:
今天准备签发一张证书,结果发现提示错误:
acme.sh is using ZeroSSL as default CA now.
Please update your account with an email address first.
然后去 Github 上项目看了下,发现一篇公告,说是从 8 月 1 日起,默认 CA 将换成 ZeroSSL,不过这不是还没到 8 月 1 日么…虽然 ZeroSSL 以前了解过也不错,同样支持免费签发 90 天期限的证书,包括通配符和 ECC 证书,甚至还支持 IP 证书,不过需要注册账号才行。至于为什么变成了 ZeroSSL,从这篇公告来看,应该是 acme.sh 被 apilayer 收购了。不过我还是习惯了 Let’s Encrypt 的方便,想要继续使用 Let’s Encrypt 证书,公告里也给出了两种解决方案。
第一种是签发证书时指定 CA:
acme.sh --issue --dns dns_cf -d domain.tld --server letsencrypt
另一种是直接更改默认 CA:
acme.sh --set-default-ca --server letsencrypt
如果设置了默认的 CA,以后就算版本升级也将一直默认使用指定的 CA。
大概 30s 左右就能成功签发证书,证书生成后会将你前面提供的 API 信息自动记录下来,将来在使用的时候就不需要再次指定了,直接生成:
acme.sh --issue -d mydomain2.com --dns dns_cf
安装证书
默认生成的证书都放在安装目录下:~/.acme.sh/
,建议使用 --install-cert
命令指定目标位置,将证书文件复制到相应的位置,这里用 Nginx 示例:
acme.sh --install-cert -d domain.tld \
--key-file /path/to/ssl/private.key \
--fullchain-file /path/to/ssl/fullchain.pem \
--reloadcmd "service nginx restart"
如果是安装 ECC 证书:
acme.sh --install-cert -d domain.tld --ecc \
--key-file /path/to/ssl/private.key \
--fullchain-file /path/to/ssl/fullchain.pem \
--reloadcmd "service nginx restart"
这里指定的所有参数都会被自动记录下来,并在将来证书自动更新以后,被再次自动调用,目前证书在 60 天以后会自动更新,无需任何操作。
如果要撤销一个证书,使用:
acme.sh --list
acme.sh --revoke -d domain.tld
acme.sh --revoke -d domain.tld --ecc
如果要删除一个证书,使用:
acme.sh --list
acme.sh --remove -d domain.tld
acme.sh --remove -d domain.tld --ecc
更新 acme.sh
升级 acme.sh 到最新版:
acme.sh --upgrade
开启自动升级:
acme.sh --upgrade --auto-upgrade
关闭自动更新:
acme.sh --upgrade --auto-upgrade 0
关于自动更新证书失败
可能会有多种情况导致更新失败,一是如果时国内服务器自动更新可能会遇到各种玄学网络问题,二是前面提到的 acme.sh 更换了默认的 CA,如果你手动签发一次会发现相关的错误信息,解决办法已在前文提及,而我主要遇到的时另一种情况。
其实以前就发现过自动更新证书失败的情况,但服务器和域名比较多,有的成功有的失败就没太在意。因为我基本上都用的 Cloudflare API 进行签发,最近发现有部分域名 DNS 设置里面出现了几十条用于认证的 TXT 记录(吐槽一下,Cloudflare 不能多选删除,一条一条的删累死了…),于是准备研究一下问题出在哪里。
通过前面大量的 TXT 记录可以推断出 API 是调用成功了的,但却签发失败了,于是直接打开 .acme.sh/account.conf
文件,发现里面记录的 API Token 居然只有一个域名的,然后在 Github 上的一条 issue 中发现了问题所在,acme.sh 默认只会保留最新的域名 Token 信息,如果你在一台服务器上设置了多个域名,那么新设置的域名 Token 就会直接覆盖掉前面的,导致前面的域名更新证书时调用的是新域名的 Token,自然就会认证失败,而且由于失败后多次尝试,也会在最新域名的 DNS 里留下大量的 TXT 记录。
简单来说就是同一个 DNS 服务商通过 API 签发证书,只能保存一个域名的 Token 信息。而解决方法有三种,推荐前两种:
-
使用不同的 Linux 用户进行证书申请。
-
签发时指定 home 文件路径,通过添加
--config-home
参数实现:
mkdir /root/.acme2
acme.sh --config-home /root/.acme2 --issue --dns dns_cf -d domain.tld
此时你的 crontab
自动任务也要做出对应的修改,新增一个任务,并且时间最好和默认的 acme.sh 任务错开:
# 默认任务
20 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
# 新增任务
40 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" --config-home "/root/.acme2" > /dev/null
- 签发证书时指定配置文件,通过添加
--accountconf
参数实现:
acme.sh --issue --dns dns_cf -d domain.tld --accountconf /root/.acme.sh/account-custom.conf
同样,crontab
自动任务也要做出对应的修改。
一个完整的演示
为了使整个流程更加清晰方便自己操作,这里贴一个自己平时使用 acme.sh 申请证书的完整流程。
- 申请方式:DNS 认证(使用 Cloudflare API)
- 申请证书 CA:Let’s Encrypt
- 申请证书类型:RSA + ECC 两个通配符证书
- * 同一台主机多个域名的情况
# 安装 acme.sh
curl https://get.acme.sh | sh
# 刷新 shell 变量配置
source ~/.bashrc
# 导入 Cloudflare API Token
export CF_Token="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export CF_Account_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export CF_Zone_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 设置默认 CA 为 Let's Encrypt
acme.sh --set-default-ca --server letsencrypt
# 签发 RSA 证书
acme.sh --issue --dns dns_cf -d domain.tld -d *.domain.tld
# 安装 RSA 证书到指定路径
acme.sh --install-cert -d domain.tld \
--key-file /path/to/ssl/domain.tld_private.key \
--fullchain-file /path/to/ssl/domain.tld_chain.pem \
--reloadcmd "systemctl restart nginx"
# 签发 ECC 证书,若不需要,跳过即可
acme.sh --issue --dns dns_cf -d domain.tld -d *.domain.tld --keylength ec-256
# 安装 ECC 证书到指定路径
acme.sh --install-cert -d domain.tld --ecc \
--key-file /path/to/ssl/ecc_domain.tld_private.key \
--fullchain-file /path/to/ssl/ecc_domain.tld_chain.pem \
--reloadcmd "systemctl restart nginx"
# 取消环境变量
unset CF_Token
unset CF_Account_ID
unset CF_Zone_ID
# 至此证书安装完毕
# 下面是同一台主机申请第二个域名证书
# 导入另一个 Cloudflare API Token
export CF_Token="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export CF_Account_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export CF_Zone_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 新建一个文件夹作为 config home,默认会把所有文件存在这里
mkdir /root/.acme2
# 设置默认 CA 为 Let's Encrypt
acme.sh --config-home /root/.acme2 --set-default-ca --server letsencrypt
# 签发 RSA 证书
acme.sh --config-home /root/.acme2 --issue --dns dns_cf -d domain.tld -d *.domain.tld
# 安装 RSA 证书到指定路径
acme.sh --config-home /root/.acme2 \
--install-cert -d domain.tld \
--key-file /path/to/ssl/domain.tld_private.key \
--fullchain-file /path/to/ssl/domain.tld_chain.pem \
--reloadcmd "systemctl restart nginx"
# 签发 ECC 证书,若不需要,跳过即可
acme.sh --config-home /root/.acme2 --issue --dns dns_cf -d domain.tld -d *.domain.tld --keylength ec-256
# 安装 ECC 证书到指定路径
acme.sh --config-home /root/.acme2 \
--install-cert -d domain.tld --ecc \
--key-file /path/to/ssl/ecc_domain.tld_private.key \
--fullchain-file /path/to/ssl/ecc_domain.tld_chain.pem \
--reloadcmd "systemctl restart nginx"
# 手动添加另一个 cron 任务
# crontab -e
50 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" --config-home "/root/.acme2" > /dev/null