概述
前情提要:
这会是一篇系列文章,本想先从 SSL 基本概念科普讲起,结果坑越挖越大。为了不鸽太久,就先把最近刚更完的证书相关操作先来逐步填坑,等后续再反补科普向内容(但愿)。
本系列内容大致分为四篇:
- SSL 相关基础知识-科普向 (有生之年)
- acme.sh 快速入门-主要功能及申请SSL证书实践
- acme.sh 最佳实践-自动申请证书与自动部署群晖 DSM (本篇)
- acme.sh 进阶版-多域名自动化管理
- acme.sh 阶段性终结-批量部署
本篇文章将基于之前入门篇介绍的基本功能,结合最佳实践,进一步深入讲解如何自动签发和续期证书,以及如何通过该工具部署和更新群晖 DSM 上的证书。
环境准备
在开始”最佳实践”之前,先来一下后续执行 acme.sh 的相关环境,方便还没阅读过前文的读者直接上手。
基础环境
在开始之前需要升级系统的 CA 证书,以避免后续在申请 SSL 证书时遇到问题。
yum install -y ca-certificates
安装 acme.sh
在本例中将以 root 用户进行安装。
export ACME_HOME="/usr/local/acme.sh"
mkdir -p $ACME_HOME/data
cd /usr/local/src
curl -so acme.sh https://raw.githubusercontent.com/acmesh-official/acme.sh/master/acme.sh
chmod +x acme.sh && ./acme.sh --install-online \
--home $ACME_HOME \
--cert-home $ACME_HOME/data \
--accountemail "nestealin@gmail.com" \
--nocron
ls ./acme.sh && rm -f ./acme.sh && cd $ACME_HOME && ls -l
参数释义:
--home
: 表示安装主目录(默认含config-home
),用于安装acme.sh
。默认情况下,它安装在~/.acme.sh
。--cert-home
: 用于保存你签发的证书 ( 如:/usr/local/acme.sh/data/
)。默认情况下,它跟随--config-home
默认值。--accountemail
: 用于在 Let’s Encrypt 注册账户的电子邮件地址,以便后续收到续期通知邮件。--nocron
: 安装acme.sh
后不自动创建定时任务。
更换签发 CA
注: 该步骤非必须,可以根据实际情况按需修改。
由于 acme.sh 默认向 ZeroSSL CA 申请 SSL 证书。然而,本次笔者将其修改为 Let’s Encrypt 作为证书的默认 CA。
acme.sh --set-default-ca --server letsencrypt
以上,完成环境的基本准备,可以开始实践操作。
最佳实践
1. 使用 Cloudflare API Token 自动签发及更新 SSL 证书
前置说明:
- 本方法所使用的是 Cloudflare API Token 方式对个别 zone 实现 DNS 单独管理;
- 如果有多个 zone 或使用 Global API Key 可参考: dnsapi · acmesh-official/acme.sh Wiki · GitHub
获取 API Token
- 访问 Cloudflare 控制台 获取 token。出于安全考虑,建议不要使用秘钥,而是为 Certbot / acme.sh 创建单独的 API 令牌。
- 选择 API 令牌模板为“编辑区域 DNS”
- 填写令牌信息,选择要管理的域名(Zone)
启用编辑设定”Token 名称”(例如: “For Certbot Auto Confirm”)
- 确认摘要信息
- 创建成功,请妥善保管你的 token (红框内容已脱敏)
记下最后创建的 API Token。
获取账户 ID
进入 zone 概述页面,右下角(下图红框)即可看到:
记下账户 ID(Account ID)。
编辑隐私文件
注: 在本文中,acme.sh 的安装目录在
/usr/local/acme.sh
vim /usr/local/acme.sh/account.conf
将获取到的 Cloudflare API Token 以如下格式追加到文件中
export CF_Token="<API Token>"
export CF_Account_ID="<Account ID>"
然后保存并退出即可
申请证书
通过执行以下命令实现自动签发 SSL 证书:
acme.sh --set-default-ca --server letsencrypt
acme.sh --issue \
--dns dns_cf \
-d "nestealin.com" \
-d "*.nestealin.com"
命令释义:
--set-default-ca --server letsencrypt
: 显式告诉 acme.sh 修改 SSL 证书默认申请的 CA 为 Let’ Encrypt;(默认为ZeroSSL)--issue
: 申请证书参数,如果需要续期可将参数改为--renew
即可;--dns dns_cf
: 使用 Cloudflare 的 DNS API 模块自动申请证书;-d 域名
: 用来声明 SSL 证书所需要签发的域名有哪些,如有多个域名则继续追加-d
参数即可;(具体限制详见: )
执行过程参考:
# 部分内容有做脱敏
[Sat Aug 31 16:03:26 CST 2024] Using CA: https://acme-v02.api.letsencrypt.org/directory
[Sat Aug 31 16:03:26 CST 2024] Creating domain key
[Sat Aug 31 16:03:26 CST 2024] The domain key is here: /usr/local/acme.sh/data/nestealin.com_ecc/nestealin.com.key
[Sat Aug 31 16:03:26 CST 2024] Multi domain='DNS:nestealin.com,DNS:*.nestealin.com'
[Sat Aug 31 16:03:30 CST 2024] Getting webroot for domain='nestealin.com'
[Sat Aug 31 16:03:30 CST 2024] Getting webroot for domain='*.nestealin.com'
[Sat Aug 31 16:03:30 CST 2024] Adding TXT value: YzuIQsaUWFaPxiYmHfMEvW2spm3Ezz2cpZ0R02F-7F8 for domain: _acme-challenge.nestealin.com
[Sat Aug 31 16:03:34 CST 2024] Adding record
[Sat Aug 31 16:03:35 CST 2024] Added, OK
[Sat Aug 31 16:03:35 CST 2024] The TXT record has been successfully added.
[Sat Aug 31 16:03:35 CST 2024] Adding TXT value: iS4HZJtsG9e-2iQyINf6r3N1PUbkpWD2_3g_c_vf8Dw for domain: _acme-challenge.nestealin.com
[Sat Aug 31 16:03:38 CST 2024] Adding record
[Sat Aug 31 16:03:39 CST 2024] Added, OK
[Sat Aug 31 16:03:39 CST 2024] The TXT record has been successfully added.
[Sat Aug 31 16:03:39 CST 2024] Let's check each DNS record now. Sleeping for 20 seconds first.
[Sat Aug 31 16:04:00 CST 2024] You can use '--dnssleep' to disable public dns checks.
[Sat Aug 31 16:04:00 CST 2024] See: https://github.com/acmesh-official/acme.sh/wiki/dnscheck
[Sat Aug 31 16:04:00 CST 2024] Checking nestealin.com for _acme-challenge.nestealin.com
[Sat Aug 31 16:04:02 CST 2024] Success for domain nestealin.com '_acme-challenge.nestealin.com'.
[Sat Aug 31 16:04:02 CST 2024] Checking nestealin.com for _acme-challenge.nestealin.com
[Sat Aug 31 16:04:03 CST 2024] Success for domain nestealin.com '_acme-challenge.nestealin.com'.
[Sat Aug 31 16:04:03 CST 2024] All checks succeeded
[Sat Aug 31 16:04:03 CST 2024] Verifying: nestealin.com
[Sat Aug 31 16:04:04 CST 2024] Pending. The CA is processing your order, please wait. (1/30)
[Sat Aug 31 16:04:08 CST 2024] Success
[Sat Aug 31 16:04:08 CST 2024] Verifying: *.nestealin.com
[Sat Aug 31 16:04:09 CST 2024] Pending. The CA is processing your order, please wait. (1/30)
[Sat Aug 31 16:04:13 CST 2024] Success
[Sat Aug 31 16:04:13 CST 2024] Removing DNS records.
[Sat Aug 31 16:04:13 CST 2024] Removing txt: YzuIQsaUWFaPxiYmHfMEvW2spm3Ezz2cpZ0R02F-7F8 for domain: _acme-challenge.nestealin.com
[Sat Aug 31 16:04:17 CST 2024] Successfully removed
[Sat Aug 31 16:04:17 CST 2024] Removing txt: iS4HZJtsG9e-2iQyINf6r3N1PUbkpWD2_3g_c_vf8Dw for domain: _acme-challenge.nestealin.com
[Sat Aug 31 16:04:21 CST 2024] Successfully removed
[Sat Aug 31 16:04:21 CST 2024] Verification finished, beginning signing.
[Sat Aug 31 16:04:21 CST 2024] Let's finalize the order.
[Sat Aug 31 16:04:21 CST 2024] Le_OrderFinalize='https://acme-v02.api.letsencrypt.org/acme/finalize/123123/456789'
[Sat Aug 31 16:04:23 CST 2024] Downloading cert.
[Sat Aug 31 16:04:23 CST 2024] Le_LinkCert='https://acme-v02.api.letsencrypt.org/acme/cert/abcd'
[Sat Aug 31 16:04:24 CST 2024] Cert success.
-----BEGIN CERTIFICATE-----
......
......
....==
-----END CERTIFICATE-----
[Sat Aug 31 16:04:24 CST 2024] Your cert is in: /usr/local/acme.sh/data/nestealin.com_ecc/nestealin.com.cer
[Sat Aug 31 16:04:24 CST 2024] Your cert key is in: /usr/local/acme.sh/data/nestealin.com_ecc/nestealin.com.key
[Sat Aug 31 16:04:24 CST 2024] The intermediate CA cert is in: /usr/local/acme.sh/data/nestealin.com_ecc/ca.cer
[Sat Aug 31 16:04:24 CST 2024] And the full-chain cert is in: /usr/local/acme.sh/data/nestealin.com_ecc/fullchain.cer
如上述输出,全程由 acme.sh 的 DNS API 模块自动完成两次 DNS Challenge 验证,并最终签发证书。
注: DNS API 模块除了在验证过程中自动添加 TXT 记录外,在验证通过后还会自动删除对应解析记录,避免残留不必要的解析记录。
至此,就已经完成了域名证书的自动签发,可以根据输出提示拿到证书及私钥文件使用了。
注: 在 Nginx 使用场景中,建议使用
fullchain.cer
作为证书文件,因为它包含了域名证书和中间证书(即具备完整证书链),从而确保客户端能够正确验证服务器的身份。
证书续期
对于证书续期,完全参照”申请证书”的步骤即可,全程也是由 DNS API 模块完成自动化操作。
acme.sh --renew \
--dns dns_cf \
-d "nestealin.com" \
-d "*.nestealin.com"
默认情况下,当证书距离到期日大于 30 天是不会进行续期操作的,当然也可以增加 --force
参数来实现强制更新。更新后的证书将直接覆盖原有的证书内容。
2. 将证书部署到群晖 DSM | 更新群晖 DSM 证书
在群晖 DSM 中,默认情况下只有 Synology 默认证书,此时如果想以自有域名将群晖服务对外暴露,此时就需要用到我们申请的 SSL 证书。
然而,默认情况下只能通过页面后台进行替换,但对于三个月一换的证书来说还是非常繁琐的。恰好 acme.sh 也集成了群晖的证书部署模块,所以下文将介绍如何使用该模块进行远程部署与更新群晖 DSM 证书。
acme.sh 集成了群晖的三种更新方式:
- 支持临时管理员更新(推荐,但只能将 acme.sh 部署在群晖本机上)
- 支持用户直接使用账号密码登录,即”常规登录“;
- 支持基于 OTP 的双重验证登录,但是对于脚本执行上会与”常规登录”有配置差异,请在使用时注意;
可以根据实际情况自行选择,由于笔者不想把群晖当做”有状态”服务器来维护,所以本次将主要介绍如何使用”常规登录“的方式,即通过 HTTPS 远程(管理机)登录的方式,部署/更新群晖 DSM 证书。
常规登录
前置说明:
- 本样例中,群晖属于内网后端,有前置的 Nginx 实现反向代理,即访问群晖域名时,会先经过独立的 Nginx 才会访问到群晖;
- 在执行 acme.sh 脚本节点上存在已签发证书;
创建独立的管理用户
为了管理方便,避免管理员变动影响证书更新,在本示例中新建了一个独立的管理用户来维护证书更新。
首先,登陆群晖 DSM,进入控制面板 - 用户与群组 - 用户账号 - 新增
简单输入账号及密码
需要加入管理员群组(administrators)
文件夹访问权限、配额、速率限制保持默认即可
应用访问权限可以先全部拒绝,只开放 DSM 权限即可
权限总览
点击保存后完成管理员账号创建
然后进入控制面板 - 用户与群组 - 用户账号 - 找到并选择刚才创建的管理员账号 - 编辑
在应用程序中找到 DSM,将”允许”改为”按 IP“
依次选择”添加 IP 地址“-“来源 IP“-“子网“,然后填写管控机所在的网络段即可 (或者限制单一主机)
注: 由于是内部管控用户,所以建议按 IP / 子网来限制网络登录。
最后依次保存退出即可。
创建执行脚本
为了后续能够通过定时任务自动执行,所以采取脚本形式包装执行内容。
创建并进入脚本路径,创建公共变量(var
)及部署脚本文件(ssl_cert_deploy_to_synologyDSM.sh
):
mkdir -p /usr/local/acme.sh/scripts
cd /usr/local/acme.sh/scripts
cat << EOF > var
#!/bin/bash
# ACME ENV
export ACME_HOME="/usr/local/acme.sh"
export PATH=\$ACME_HOME:\$PATH
export ACME_SSL_CERT_DATA_PATH="\$ACME_HOME/data"
EOF
cat << EOF > ssl_cert_deploy_to_synologyDSM.sh
#!/bin/bash
# Load ENV
SCRIPT_PATH=\$(cd \$(dirname "\$0");pwd)
. \$SCRIPT_PATH/var
export CERT_DOMAIN="example.com"
export LOCAL_SSL_CERT_FILE="\$ACME_SSL_CERT_DATA_PATH/\${CERT_DOMAIN}_ecc/fullchain.cer" # 默认签发ECC证书
export SYNO_SCHEME="https"
export SYNO_HOSTNAME="synology.example.com"
export SYNO_PORT="443"
export SYNO_Username="acme-user"
export SYNO_Password="123456789ABCDEFG"
export SYNO_Certificate=\$CERT_DOMAIN # 可选,描述证书的名称
export SYNO_Create=1 # 如果证书不存在则创建
function check_local_cert() {
# 检查本地证书路径,如果不存在则提示需要执行证书签发
if [ ! -f "\$LOCAL_SSL_CERT_FILE" ]; then
# 目录不存在,打印需要执行的命令
echo "Error: Cert file \$LOCAL_SSL_CERT_FILE does not exist."
echo "Please execute the following command to issue the certificate:"
echo "./acme.sh --issue --dns dns_cf -d '\$CERT_DOMAIN' -d '*.\$CERT_DOMAIN'"
exit 1
else
echo "Cert file \$LOCAL_SSL_CERT_FILE exists."
fi
}
function deploy_ssl_cert_to_synology_DSM() {
echo "Deploying SSL Cert: \$CERT_DOMAIN to synology DSM."
deploy_status=\$( cd \$ACME_HOME && acme.sh --deploy --home \$ACME_HOME -d "\$CERT_DOMAIN" --deploy-hook synology_dsm )
if [ \$? -eq 0 ]; then
echo "证书更新成功: \$deploy_status"
else
echo "错误信息: \$deploy_status"
exit 1
fi
}
check_local_cert
deploy_ssl_cert_to_synology_DSM
EOF
chmod +x ssl_cert_deploy_to_synologyDSM.sh
参数释义:
CERT_DOMAIN
: 在 acme.sh 维护签发的证书名称,详见:acme.sh --list
结果中的”Main_Domain“;SYNO_SCHEME
: 登陆群晖的 HTTP 协议,由于做了强制 HTTPS 所以此处使用 HTTPS 作为交互协议;SYNO_HOSTNAME
: 登陆群晖的请求域名,由于前置了 Nginx,所以此处使用域名交互;SYNO_PORT
: 登陆群晖的端口,由于登录协议采用了 HTTPS 并且在前置 Nginx 上监听的是 HTTPS 的默认端口,所以在脚本中填写 443,如果使用其他端口直接修改即可;SYNO_Username
: 群晖中的管理员账户,即第一步创建的管理账户名称;SYNO_Password
: 群晖中的管理员账户密码,即第一步创建的管理账户密码;SYNO_Certificate
: 用于在群晖证书中显示的”描述“名称,这里采取与证书域名相同名称进行填充,可选项;SYNO_Create
: 用于声明群晖证书中如果不存在证书是否自动创建,如果值为 1 则表示自动创建,默认不自动创建,可选项;
注: 脚本内容已对敏感参数进行脱敏,请根据实际情况进行修改。包括:
CERT_DOMAIN
SYNO_HOSTNAME
SYNO_Username
SYNO_Password
执行更新
通过以下命令即可完成证书的首次部署或更新部署:
cd /usr/local/acme.sh/scripts
sh ssl_cert_deploy_to_synologyDSM.sh
输出过程:
[Sun Aug 18 15:27:48 CST 2024] Logging into synology.nestealin.com:443...
[Sun Aug 18 15:27:50 CST 2024] Getting certificates in Synology DSM...
[Sun Aug 18 15:27:50 CST 2024] Generating form POST request...
[Sun Aug 18 15:27:50 CST 2024] Upload certificate to the Synology DSM.
[Sun Aug 18 15:27:50 CST 2024] Restart HTTP services failed.
[Sun Aug 18 15:27:51 CST 2024] Success
出现以上输出则代表执行成功。
结果验证
登陆 DSM 确认证书已更新: 登陆 DSM - 控制面板 - 安全性 - 证书
在 Synology Drive 查看执行状态
上传结果正常,表明证书更新正常。
2FA 登陆 | OTP 验证
如果群晖的管理员开启了双重认证,则需要通过以下方式进行配置。
在”常规登录”的基础上,需要额外声明 SYNO_DEVICE_ID
和 SYNO_OTP_CODE
参数使用。
具体详见: deployhooks · acmesh-official/acme.sh Wiki · GitHub
ref: Synology NAS Guide · acmesh-official/acme.sh Wiki · GitHub
3. 定时自动更新 SSL 证书
由于”最佳实践1. 使用 Cloudflare API Token 自动签发及更新 SSL 证书“已经完成了 SSL 证书全流程的自动申请,这时候我们只需要利用 --renew-all
参数即可对所有已申请的证书统一执行续期,然后让定时任务(crontab)定期执行即可。
cat << EOF >> /var/spool/cron/`whoami`
# Ansible acme.sh Auto Renew Cert
## Per 2 month
0 1 1 */2 * /bin/bash /usr/local/acme.sh/acme.sh --renew-all --ecc > /dev/null 2>&1
EOF
执行说明:
0 1 1 */2 *
: 表示每两个月的第一天凌晨 1 点整执行一次。- 即该 cron 表达式实际上表示的是,每隔两个月,在每个月的 1 号凌晨 1 点执行任务。例如,如果从 1 月份开始执行,那么下一次执行将是 3 月份的 1 号凌晨 1 点,然后是 5 月份的 1 号,以此类推。
注意:
--renew-all
命令用于统一续期证书,其执行范围仅限于 acme.sh 所在的本Certbot来确认。- 默认情况下,**
renew
命令仅更新 acme.sh 的本地证书存储位置** (如:/usr/local/acme.sh/data/xxx_ecc
),如果证书有在其他路径使用,如: 群晖 DSM 或 Nginx 等,仍需要在每次证书自动更新后手动更新/部署。
可能遇到的问题
too many certificates already issued
如果你在使用acme.sh申请证书时遇到”too many certificates already issued”的错误,可能是因为你超过了这些速率限制
[Sun Aug 18 18:16:14 CST 2024] Using CA: https://acme-v02.api.letsencrypt.org/directory
[Sun Aug 18 18:16:14 CST 2024] Creating domain key
[Sun Aug 18 18:16:14 CST 2024] The domain key is here: /usr/local/acme.sh/nestealin.com_ecc/nestealin.com.key
[Sun Aug 18 18:16:14 CST 2024] Multi domain='DNS:nestealin.com,DNS:*.nestealin.com'
[Sun Aug 18 18:16:15 CST 2024] Error creating new order. Le_OrderFinalize not found. {
"type": "urn:ietf:params:acme:error:rateLimited",
"detail": "Error creating new order :: too many certificates (5) already issued for this exact set of domains in the last 168 hours: *.nestealin.com,nestealin.com, retry after 2024-08-19T15:38:38Z: see https://letsencrypt.org/docs/duplicate-certificate-limit/",
"status": 429
}
[Sun Aug 18 18:16:15 CST 2024] Please add '--debug' or '--log' to see more information.
[Sun Aug 18 18:16:15 CST 2024] See: https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh
Cannot find DNS API hook for: dns_cf
问题描述
在使用 DNS 期望自动添加解析并签发证书时,出现如下错误:
[Sat Aug 17 23:04:08 CST 2024] Please make sure to prepend '_acme-challenge.' to your domain
[Sat Aug 17 23:04:08 CST 2024] so that the resulting subdomain is: _acme-challenge.nestealin.com
[Sat Aug 17 23:04:08 CST 2024] Cannot find DNS API hook for: dns_cf
[Sat Aug 17 23:04:08 CST 2024] You need to add the TXT record manually.
[Sat Aug 17 23:04:08 CST 2024] Add the following TXT record:
[Sat Aug 17 23:04:08 CST 2024] Domain: '_acme-challenge.nestealin.com'
[Sat Aug 17 23:04:08 CST 2024] TXT value: '...-of7w'
[Sat Aug 17 23:04:08 CST 2024] Please make sure to prepend '_acme-challenge.' to your domain
[Sat Aug 17 23:04:08 CST 2024] so that the resulting subdomain is: _acme-challenge.nestealin.com
[Sat Aug 17 23:04:08 CST 2024] Please add the TXT records to the domains, and re-run with --renew.
[Sat Aug 17 23:04:08 CST 2024] Please add '--debug' or '--log' to see more information.
[Sat Aug 17 23:04:08 CST 2024] See: https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh
意味着需要手动添加解析。
原因
- 安装过程有所遗漏,没有使用
--install-online
参数安装依赖。
解决方式
- 重新安装 acme.sh,并在安装时指定
--install-online
参数。
./acme.sh --install-online
总结
通过本篇前两个自动化实践内容,我们实现了证书的自动申请与部署。这一改进不仅简化了证书管理流程,还通过设置定时任务,实现了单个域名证书签发、部署及更新的自动化闭环管理,从而大幅降低了用户的维护成本。