给 QNAP 配置 ACME 那些事

QNAP NAS 使用过程中续签 SSL 证书是一个比较麻烦的事情,因为 QNAP 在国内使用的 ACME 不支持通过 DNS TXT 记录验证,只能通过 HTTP 验证,这就导致了无法使用默认的 Let’s Encrypt 证书续签方式。

因此,基于 QNAP 良好的 Docker 兼容性,我们可以使用 Docker 容器来运行 ACME 客户端,然后通过 DNS 验证来续签证书。

在后续的内容中,<user> 表示你的用户名,<nas_ip> 表示你的 QNAP NAS 的 IP 地址,your.example.com 表示你想要给 NAS 使用的域名。

设置 SSH 登录

在后续的自动化中,acme 容器需要通过 SSH 登录到 QNAP NAS 中,因此需要设置 SSH 登录。

首先,为它创建一个 SSH 密钥对:ssh-keygen -t ed25519 -f ./acme_key -C "acme",这句命令会在当前目录下生成两个文件:acme_keyacme_key.pub,分别是私钥和公钥,之后按照以下步骤设置 SSH 登录:

  1. 网路和文件服务 -> Telnet/SSH 中启用 SSH 服务,然后编辑访问权限,启用用户 <user>
  2. <user> 用户登录 QNAP,用户头像 -> 登录与安全性 -> SSH -> SSH 密钥,添加 ./acme_key.pub
  3. 在本地测试一下是否可以登录:ssh -i ./acme_key <user>@<nas_ip>

选择一个数据目录

在 QNAP 中,我们需要选择一个目录用于存放 ACME 容器的数据,后续会在这个目录下生成证书等文件。

以我自己的配置为例,我具有一个共享文件夹 Config,用于存放一些配置文件,因此我在这个目录下创建了一个 ACME 目录,用于存放 ACME 容器的数据。

在实际的目录中,它对应了 /share/Config/ACME你可以根据自己的情况选择并创建一个目录来代替这一路径进行后续操作

  1. 使用默认用户采用 SSH 登录到 QNAP 中。
  2. 使用 cd /share/Config/ACME 进入到 ACME 目录。
  3. 使用 vim certbot.key 创建一个空文件,并将上述 SSH 密钥对中的私钥 acme_key 的内容复制到这个文件中。
  4. 使用 ESC -> :wq 保存并退出。
  5. 使用 sudo chmod 600 certbot.key 修改文件权限。
  6. 使用 sudo chown admin:administrators certbot.key 修改文件所有者为 admin,也即容器内的 root 用户。

配置 ACME 容器

在创建好了上述的准备工作之后,我们可以开始配置 ACME 容器了。

Container Station 中,选择 应用程序 -> 创建,名称使用 acmesh,YAML 配置使用以下内容:

version: "3"

services:
  server:
    image: neilpang/acme.sh
    command: ["daemon"]
    container_name: acmesh
    restart: always
    network_mode: host
    volumes:
      - "/share/Config/ACME:/acme.sh"

通过采用 network_mode: host 的方式,可以让 ACME 容器直接使用 127.0.0.1 来访问 QNAP 的 SSH 服务,不需要额外的解析。

同时,这里将 /share/Config/ACME 映射到了 /acme.sh,你应当根据自己的实际情况修改这个路径。

在创建成功后,应该能够在容器列表中看到 acmesh 容器正常运行。

配置 ACME 证书

在容器正常运行之后,可以使用默认用户采用 SSH 登录到 QNAP 中,并使用 docker exec -it acmesh /bin/sh 进入到容器内部。

在容器内部,可以使用 acme.sh --help 查看帮助信息,这里我们需要配置 DNS API 来进行验证,相关的 DNS API 配置可以参考官方文档

作为参考,我使用的是 Cloudflare 的 DNS API,同时希望使用 ECC 证书,因此我使用了以下命令,其中 your.example.com 表示你的域名:

export CF_Token="XXXXXXXXXXXXXXXX"
export CF_Zone_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
acme.sh --issue --dns dns_cf -d 'your.example.com' -d '*.your.example.com' --keylength ec-384 --days 85 --server letsencrypt

在预期的情况下,你应当能够正常签发证书,同时在 /acme.sh 目录下看到目录结构:

/acme.sh
├── account.conf
├── ca
├── certbot.key
├── http.header
└── your.example.com_ecc # your domain
    ├── ca.cer
    ├── fullchain.cer
    ├── your.example.com.cer
    ├── your.example.com.conf
    ├── your.example.com.csr
    ├── your.example.com.csr.conf
    └── your.example.com.key

创建部署脚本

在签发证书之后,我们需要将证书部署到 QNAP 的 Web 服务中,以便于使用。首先,先测试我们的 SSH 登录是否正常:

ssh -i /acme.sh/certbot.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null <user>@127.0.0.1

如果能够正常登录,那么我们可以继续创建部署脚本。

/share/Config/ACME 目录下,创建一个 deploy.sh 文件,用于部署证书。注意,这里的 your.example.com 需要替换为你的域名:

#!/bin/sh

if [ "$1" = "--dry-run" ]; then
  DRY_RUN="true"
fi

# check if root and not dry-run
if [ -z "$DRY_RUN" ] && [ "$(id -u)" -ne 0 ]; then
  echo "Please run as root"
  exit 1
fi

DOMAIN="your.example.com"
CERT_ROOT="/share/Config/ACME/$DOMAIN"

# check is ecc
if [ -d "$CERT_ROOT"_ecc ]; then
  CERT_ROOT="$CERT_ROOT"_ecc
fi


KEY_FILE="/etc/stunnel/backup.key.def"
CERT_FILE="/etc/stunnel/backup.cert.def"
CA_FILE="/etc/stunnel/uca.pem"

NEW_KEY_FILE="$CERT_ROOT/$DOMAIN.key"
NEW_CERT_FILE="$CERT_ROOT/$DOMAIN.cer"
NEW_CA_FILE="$CERT_ROOT/ca.cer"

# avoid `jq: command not found`
export PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/sbin:/usr/local/bin"

run_command() {
  local cmd=$1
  # check if dry-run
  echo "[-] RUN: $cmd"
  if [ "$DRY_RUN" != "true" ]; then
    eval $cmd
  fi
}

echo "[+] Copy new key and cert"

run_command "cp $NEW_KEY_FILE $KEY_FILE"
run_command "cp $NEW_CERT_FILE $CERT_FILE"
run_command "cp $NEW_CA_FILE $CA_FILE"

echo "[+] Restart stunnel"

run_command "/etc/init.d/stunnel.sh generate_cert_key"
run_command "/etc/init.d/stunnel.sh restart"
run_command "/etc/init.d/reverse_proxy.sh reload"

echo "[+] Done"

保存并退出之后,使用 sudo chmod +x deploy.sh 修改文件权限。

在进行下一步之前,请确保你已经将 /etc/stunnel/backup.key.def/etc/stunnel/backup.cert.def 备份好,以防止证书丢失。

配置 sudo 权限

由于 ACME 容器需要执行一些需要 admin 权限的操作,因此需要配置 sudo 权限。

但是默认情况下,QNAP 中的 sudo 需要输入密码,为了自动化操作的便捷,可以配置 sudo 对于特定程序免密码,具体操作如下:

  1. 使用默认用户采用 SSH 登录到 QNAP 中。
  2. 使用 sudo mkdir /usr/etc/sudoers.d 创建 sudoers 配置目录。
  3. 使用 echo "<user> ALL=(ALL) NOPASSWD:/share/Config/ACME/deploy.sh" | sudo tee /usr/etc/sudoers.d/certbot 添加配置。

这句命令会将 <user> 用户对于 /share/Config/ACME/deploy.sh 的特权执行权限添加到 sudoers 中,同时免去密码,你应当根据自己的实际情况修改用户名和脚本路径

但是这只是一次性方案,重启后会失效,如果需要长期生效,需要编辑 flash 中的 autorun.sh,并打开 系统设置 -> 硬件 -> 启动时运行用户定义的进程,下列为具体操作和参考文档:

  1. 使用 sudo bash 切换到 root 用户。

  2. 使用 l 挂载 flash 的配置目录到 /tmp/nasconfig_tmp

  3. 使用 vi /tmp/nasconfig_tmp/autorun.sh 编辑 autorun.sh 文件,添加以下内容:

    #!/bin/bash
    mkdir -p /usr/etc/sudoers.d
    echo "<user> ALL=(ALL) NOPASSWD:/share/Config/ACME/deploy.sh" > /usr/etc/sudoers.d/certbot
  4. 正常保存并退出。

  5. 使用 chmod +x /tmp/nasconfig_tmp/autorun.sh 修改文件权限,允许执行。

  6. 使用 /etc/init.d/init_disk.sh umount_flash_config 卸载 flash。

  7. 在正确编辑后,你应当能在 系统设置 -> 硬件 下方有关链接中查看到 autorun.sh 的内容。

注意事项:

  1. 挂载时到 6 表示 flash 的第 6 个分区,不要忘记。
  2. 根据官方文档,一定要在编辑后卸载 flash

参考文档:Running Your Own Application at Startup - QNAP

启用 ACME 部署

再次使用 docker exec -it acmesh /bin/sh 进入到容器内部,运行以下命令:

export DEPLOY_SSH_USER='<user>'
export DEPLOY_SSH_SERVER='127.0.0.1'
export DEPLOY_SSH_CMD='ssh -i /acme.sh/certbot.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
export DEPLOY_SSH_BACKUP='no'
export DEPLOY_SSH_REMOTE_CMD='sudo /share/Config/ACME/deploy.sh'

并尝试进行部署:

acme.sh --deploy -d your.example.com --deploy-hook ssh

在预期的情况下,你应当能够看到证书部署成功的输出:

[+] Copy new key and cert
[-] RUN: cp /share/Config/ACME/your.example.com_ecc/your.example.com.key /etc/stunnel/backup.key.def
[-] RUN: cp /share/Config/ACME/your.example.com_ecc/fullchain.cer /etc/stunnel/backup.cert.def
[+] Restart stunnel
[-] RUN: /etc/init.d/stunnel.sh generate_cert_key
[-] RUN: /etc/init.d/stunnel.sh restart
Shutting down apache proxy: OK
Start apache proxy: OK
[-] RUN: /etc/init.d/reverse_proxy.sh reload
AH00558: reverseproxy: Could not reliably determine the server's fully qualified domain name, using **. Set the 'ServerName' directive globally to suppress this message
[+] Done

解释与收尾工作

测试是否部署成功

可以通过 curl 来测试证书是否部署成功:

curl -vkI https://your.example.com:5001

检查输出是否包含新的证书信息:

* Server certificate:
*  subject: CN=your.example.com
*  start date: May 17 21:18:04 2024 GMT
*  expire date: Aug 15 21:18:03 2024 GMT
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.

QNAP 使用证书的方式

/etc/init.d/stunnel.sh 中,有以下内容:

SSL_KEY_FILE="/etc/stunnel/backup.key"
SSL_KEY_DEF_FILE="/etc/stunnel/backup.key.def"
SSL_CERT_FILE="/etc/stunnel/backup.cert"
SSL_CERT_DEF_FILE="/etc/stunnel/backup.cert.def"
SSL_PEM_FILE="/etc/stunnel/stunnel.pem"

# ...

generate_cert_key()
{
	if [ ! -f "/etc/IS_G" ]; then
		if [ -f $SSL_KEY_DEF_FILE ] && [ -f $SSL_CERT_DEF_FILE ]; then
			/bin/cp $SSL_KEY_DEF_FILE $SSL_KEY_FILE
			/bin/cp $SSL_CERT_DEF_FILE $SSL_CERT_FILE
			/bin/cat $SSL_KEY_FILE > $SSL_PEM_FILE
			/bin/cat $SSL_CERT_FILE >> $SSL_PEM_FILE
			/bin/chmod 600 $SSL_CERT_FILE
			/bin/chmod 600 $SSL_KEY_FILE
			/bin/chmod 600 $SSL_PEM_FILE
			return
		fi
    # ...
}

从而对于 /etc/stunnel

  • backup.key.defbackup.cert.def 是存放自定义证书的文件
  • backup.keybackup.cert 是用于存放实际使用证书的文件
  • stunnel.pem 是将 backup.keybackup.cert 合并后的文件

证书定时续签

在默认情况下,ACME 容器中存在 cron 任务,用于自动续签证书。你可以通过 crontab -l 查看当前的 cron 任务:

3 16 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" --config-home "/acme.sh" > /proc/1/fd/1 2>/proc/1/fd/2

如果你需要修改 cron 任务,可以使用 crontab -e 编辑。