acme.sh实现自动申请证书与自动部署群晖DSM


概述

前情提要:

这会是一篇系列文章,本想先从 SSL 基本概念科普讲起,结果坑越挖越大。为了不鸽太久,就先把最近刚更完的证书相关操作先来逐步填坑,等后续再反补科普向内容(但愿)。

本系列内容大致分为四篇:

本篇文章将基于之前入门篇介绍的基本功能,结合最佳实践,进一步深入讲解如何自动签发和续期证书,以及如何通过该工具部署和更新群晖 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 证书

前置说明:

获取 API Token

  1. 访问 Cloudflare 控制台 获取 token。出于安全考虑,建议不要使用秘钥,而是为 Certbot / acme.sh 创建单独的 API 令牌。
  1. 选择 API 令牌模板为“编辑区域 DNS”
  1. 填写令牌信息,选择要管理的域名(Zone)

启用编辑设定”Token 名称”(例如: “For Certbot Auto Confirm”)

  1. 确认摘要信息
  1. 创建成功,请妥善保管你的 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_IDSYNO_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

总结

通过本篇前两个自动化实践内容,我们实现了证书的自动申请与部署。这一改进不仅简化了证书管理流程,还通过设置定时任务,实现了单个域名证书签发、部署及更新的自动化闭环管理,从而大幅降低了用户的维护成本。


文章作者: NesTeaLin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 NesTeaLin !
  目录