得之我幸 失之我命

when someone abandons you,it is him that gets loss because he lost someone who truly loves him but you just lost one who doesn’t love you.

HTTPS 证书申请与续期

本文记录使用 acme.sh,通过 Cloudflare DNS 验证申请 HTTPS 证书,并配置自动续期和服务重载的过程。以 Debian 上 Docker Compose 管理的 Navidrome 为例,体现如何在证书更新后配置服务自动重启

安装 acme.sh

在 Debian 上,可以通过软件包安装:

1
2
sudo apt update
sudo apt install acme.sh

检查安装结果:

1
2
acme.sh --version
command -v acme.sh

后续证书操作以普通用户执行。本文使用的域名是 demo.test.com,操作时需要替换成自己的域名和用户名

配置 Cloudflare API Token

DNS-01 验证通过添加 _acme-challenge TXT 记录,证明申请者拥有域名控制权。使用 Cloudflare API 后,acme.sh 可以自动添加和删除验证记录,所以通过 DNS-01 验证申请证书时不需要为证书申请开放服务器的入站端口,但服务器需要能够访问 Cloudflare API 和证书签发机构

在 Cloudflare API Tokens 页面创建 Token,授予以下权限:

权限 用途
Zone / DNS / Edit 添加和删除验证记录
Zone / Zone / Read 查询域名所在的区域

将资源范围限制为需要申请证书的域名,test.com,虽然申请证书的是子域名 demo.test.com,仍使用 Cloudflare 中 test.com 区域的 Zone ID

在终端设置环境变量:

1
2
export CF_Token='你的 Cloudflare API Token'
export CF_Zone_ID='你的 Zone ID'

申请证书

显式指定 Let’s Encrypt,并申请 ECC 证书:

1
2
3
4
acme.sh --issue \
--server letsencrypt \
--dns dns_cf \
-d demo.test.com

acme.sh 会调用 Cloudflare API 添加 TXT 记录,等待验证完成,再获取证书

申请成功后,输出中会列出证书文件的位置。本例生成的文件位于:

1
/home/test/.acme.sh/demo.test.com_ecc/

其中包含:

文件 内容
demo.test.com.cer 域名证书
demo.test.com.key 私钥
ca.cer 中间 CA 证书
fullchain.cer 域名证书与中间证书组成的完整证书链

后续操作这张 ECC 证书时,使用 --ecc 指定证书类型

安装证书,重启服务

证书签发后,通过 --install-cert 安装到固定目录,方便容器读取,也让 acme.sh 在续期后自动更新这些文件

创建证书目录,并让运行 acme.sh 的用户拥有写入权限:

1
2
sudo mkdir -p /etc/ssl/demo.test.com
sudo chown "$(id -un):$(id -gn)" /etc/ssl/demo.test.com

如果需要重新启动服务才能使用更新后的证书文件,可以通过 acme.sh 的 --reloadcmd,在证书安装完成后执行服务的重启命令

以本例的 navidrome 容器为例,对于 Compose 管理的服务,自动化命令可以采用 Compose 文件和服务名

假设 Compose 文件位于以下位置,使用时替换成实际路径:

1
/home/test/docker/navidrome/compose.yml

重启命令使用 Compose 配置中的服务名,例如:

1
2
3
services:
navidrome:
image: deluan/navidrome:latest

重启命令为:

1
2
3
/usr/bin/docker compose \
-f /home/test/docker/navidrome/compose.yml \
restart navidrome

使用 -f 指定 Compose 文件的绝对路径后,不需要先进入 Compose 目录。 如果启动时使用了自定义项目名或额外配置文件,重启命令也应保留相同参数

以运行 acme.sh 的用户测试重启命令,确认能够无需交互地执行后,重新安装证书并登记自动重启命令:

1
2
3
4
5
6
acme.sh --install-cert \
-d demo.test.com \
--ecc \
--key-file /etc/ssl/demo.test.com/privkey.pem \
--fullchain-file /etc/ssl/demo.test.com/fullchain.pem \
--reloadcmd "/usr/bin/docker compose -f /home/test/docker/navidrome/compose.yml restart navidrome"

这次安装也会执行重启命令,Navidrome 会短暂中断服务

部署后的证书文件为:

1
2
3
/etc/ssl/demo.test.com/
├── privkey.pem
└── fullchain.pem

⚠️ Navidrome 的容器需要挂载证书目录,并配置为读取容器内对应的证书和私钥路径。文件权限也应允许容器内的服务用户读取私钥

1
2
3
4
5
6
7
services:
navidrome:
environment:
ND_TLSCERT: /certs/fullchain.pem
ND_TLSKEY: /certs/privkey.pem
volumes:
- /etc/ssl/demo.test.com:/certs:ro

配置自动续期

1
acme.sh --install-cronjob

这里不加 sudo,让任务使用同一用户的证书配置和 Cloudflare 凭据

检查生成的任务:

1
crontab -l

确认其中存在调用 acme.sh --cron 的任务。如果已经通过其他方式配置了续期调度,应避免重复添加

可以手动运行一次检查:

1
acme.sh --cron

--cron 会检查证书是否需要续期。刚申请的证书通常还没到续期时间,因此跳过是正常结果

配置好续期定时任务后,自动流程为:

1
2
3
4
5
6
7
8
9
10
11
cron 定期执行 acme.sh --cron
↓
检查是否需要续期
↓
通过 Cloudflare DNS 验证获取新证书
↓
更新 /etc/ssl/demo.test.com/ 下的证书文件
↓
执行 Docker Compose 重启命令
↓
Navidrome 加载新证书

其他

查看证书信息:

1
acme.sh --info -d demo.test.com --ecc

也可以单独检查这张证书是否需要续期:

1
acme.sh --renew -d demo.test.com --ecc

确认 Cloudflare 凭据已保存

自动续期需要在无人登录、没有手动执行 export 的环境下完成 DNS 验证

当前 dns_cf 插件在同时设置 CF_Token 和 CF_Zone_ID 时,会将它们保存到域名配置中。因此,单独检查 account.conf 没有结果,不代表凭据没有保存

本例可以这样检查,只显示配置项名称:

1
2
3
4
sed -n \
-e 's/^CF_Token=.*/CF_Token=<已保存>/p' \
-e 's/^CF_Zone_ID=.*/CF_Zone_ID=<已保存>/p' \
/home/test/.acme.sh/demo.test.com_ecc/demo.test.com.conf

有对应输出表示配置项存在;实际续期仍要求 Token 有效且权限足够

检查证书

证书签发、文件部署和服务加载完成后,可以检查 HTTPS 入口实际返回的证书:

1
2
3
4
5
openssl s_client \
-connect demo.test.com:port \
-servername demo.test.com \
</dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates

这里检查的是该地址实际提供的证书。如果域名启用了 Cloudflare 代理,公网访问看到的是 Cloudflare 边缘证书;要确认源站证书,应把 -connect 改为源站地址,保留 -servername 为证书域名

校验证书

确认域名匹配和证书链验证,加上 域名校验和验证失败即报错的参数,并保留完整输出:

1
2
3
4
5
6
openssl s_client \
-connect demo.test.com:port \
-servername demo.test.com \
-verify_hostname demo.test.com \
-verify_return_error \
</dev/null

将域名和端口替换成实际值。成功时应看到:

1
2
3
Verification: OK
Verified peername: demo.test.com
Verify return code: 0 (ok)

这表示证书链通过本机信任库验证,且证书匹配指定域名

be slow to promise and quick to perform.