证书的签发管理应该是多台 VPS 玩家的烦恼。当然,像 MJJ 一样买小鸡只挂探针的当我没说。 我手上的小鸡不少,为了省事喜欢签通配符证书,可站点分散在不同机器上,每台都得单独签一遍。后来在网上搜到了宝藏博主七夏浅笑写的 acmeDeliver,正好解决这件事。
当初就是照着原作者的教程《使用 acme.sh 部署通配符证书申请与分发服务》搭起来的。美美哒用了一段时间,还是觉得不够懒,每台机器都要配 crontab 定时去拉。好在这两年 AI 足够发达,我前后换着用了好几个当时最强的模型,按自己的需求 vibe coding 出了 V3,这里记录一下。
项目地址
原版 julydate/acmeDeliver
原作者教程 《使用 acme.sh 部署通配符证书申请与分发服务》
我魔改的 V3 Catker/acmeDeliver
先感谢原作者七夏浅笑,还有给原版贡献了 client.sh 客户端脚本的 Raoby。V3 是在原版的基础上改出来的。
关于原版
原作者的出发点在文章里讲得很清楚。他不想把 DNS 服务商的 API Key 放到每台服务器上,也不想每台机器都配一遍 acme.sh,干脆在一台机器上签发,再起一个轻量服务把证书分发出去。服务端用 Go 写成单个二进制,不用装运行环境,跨平台也方便。
- 服务端是一个 Go 写的小 Web 服务,把 acme.sh 装好的证书目录通过 HTTP 提供出去,用"密码 + 时间戳"鉴权,可以开 TLS
- 客户端是一个 Bash 脚本
client.sh,配合 cron 定时请求服务端,比对证书的更新时间戳,有新证书就下载,还能顺手部署到 Nginx 或 Apache
一台机器签发,其余机器定时来拉,DNS API 密钥只放在签发机上,这正是我想要的。
关于 V3
V3 最大的改动是把"客户端定时拉取"换成了"服务端主动推送"。
- WebSocket 推送。客户端以 daemon 形式常驻,和服务端保持一条 WebSocket 长连接。服务端用 fsnotify 盯着证书目录,证书一更新,就推给订阅了这个域名的客户端
- 按域名订阅。每个客户端只订阅自己用得到的域名,支持
*.example.com这样的通配符,写*就是全部订阅 - 断线后补推。客户端重连后会自动同步一次,另外每小时还有一次定时同步,离线期间错过的更新都能补回来
- 交付回执。客户端收到证书后回一个 ACK,服务端记下每台机器每个域名最近一次的结果,
--status一查就知道谁拿到了 - 部署前校验。写盘前先检查证书和私钥是否配对,对不上就拒收,免得坏证书部署上去把服务搞挂
- reload 失败重试。reload 命令失败后会退避重试,30 秒起步,最多 5 次,省得证书换了服务还挂着旧的
原版的 Pull 模式也留着,客户端二进制一次性拉取照样能用,习惯 cron 的可以继续这么玩。
整体结构大概是这样。
签发机(签发 + 服务端)
acme.sh 走 DNS 验证签发泛域名证书
│
--install-cert 安装到 /home/acme/<域名>/
--reloadcmd 写入 time.log
│
acmedeliver-server 监控目录变化
│ WebSocket 推送(wss)
┌─────────────┼─────────────┐
小鸡 A 小鸡 B 其他小鸡 …
(daemon) (daemon)
部署后 reload Nginx1. 证书签发
这部分基本照搬原作者教程。acme.sh 的安装、CA 选择、ECC 证书、续期通知,原文写得很细,这里不重复。我只是包了一个小脚本,输入域名后走 DNS 验证签泛域名证书,再装到服务端的证书目录。
#!/bin/bash
echo -n "Please Enter your Domain:"
read DOMAIN
acme.sh --issue --dns dns_cf -d "${DOMAIN}" -d "*.${DOMAIN}"
mkdir -p /home/acme/${DOMAIN}
acme.sh --install-cert -d "${DOMAIN}" \
--cert-file /home/acme/${DOMAIN}/cert.pem \
--key-file /home/acme/${DOMAIN}/key.pem \
--fullchain-file /home/acme/${DOMAIN}/fullchain.pem \
--reloadcmd "date +%s > /home/acme/${DOMAIN}/time.log"最后一行别漏。acme.sh 每次续期装完证书都会执行 --reloadcmd,这里把当前时间戳写进 time.log。这个约定也是从原版沿用下来的。服务端发现证书文件有变化,等 5 秒防抖就推出去,客户端则拿 time.log 里的时间戳判断自己手上那份是不是最新的。
dns_cf 是 Cloudflare 的 DNS 插件,换成自己域名服务商对应的就行,支持列表见 acme.sh 文档。2. acmeDeliver 服务端
从 Releases 下载对应架构的二进制,先生成一份配置。
./acmedeliver-server --gen-config > config.yaml我改了这几项。
port: "9090"
base_dir: "/home/acme/"
key: "一个足够长的随机密码"
tls: true
tls_port: "9443"
cert_file: "/home/acme/example.com/fullchain.pem"
key_file: "/home/acme/example.com/key.pem"
ip_whitelist: "" # 可选强烈建议开 TLS,毕竟传的是私钥。开了 TLS 以后默认只监听 9443,明文端口就不开了。
TLS 证书直接用 acme.sh 签出来的那张,续期后服务端会自己重新加载,不用重启。
再写个 systemd 服务让它常驻。
[Unit]
Description=acmeDeliver Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/home/acme/acmedeliver-server -c "/home/acme/config.yaml"
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target3. acmeDeliver 客户端
客户端配置的根节点必须是 client:,不然会直接报错。
client:
server: "wss://your-server:9443"
password: "和服务端一致的密码"
workdir: "/var/lib/acmedeliver" # 证书保存在 <workdir>/<域名>/ 下,默认就是这个
# 收到证书后执行的命令;域名没有匹配的 sites 时也会执行
default_reload_cmd: "systemctl reload nginx"
daemon:
enabled: true
reconnect_interval: 30
reload_debounce: 5
# 订阅全部域名;也可以只写需要的,如 "example.com"、"*.example.org"
subscribe:
- "*"
# 可选:需要把证书复制到别的路径时再配
sites:
- domain: "example.com"
cert_path: "/etc/realm/ssl/cert.pem"
key_path: "/etc/realm/ssl/key.pem"
fullchain_path: "/etc/realm/ssl/fullchain.pem"
reloadcmd: "systemctl restart realm"我自己有两种用法。一台小鸡跑着宝塔面板,站点的 nginx 直接引用 /var/lib/acmedeliver/<域名>/ 里的证书,配一行 default_reload_cmd: "systemctl reload nginx" 就够了,证书推过来,客户端顺手 reload Nginx。另一种是 realm 这类要求证书放在固定路径的,就在 sites 里写上目标路径和对应的 reloadcmd,客户端会复制过去再执行。
客户端也用 systemd 常驻,把 ExecStart 换成 acmedeliver-client -c /path/to/config.yaml --daemon。配置里写了 daemon.enabled: true 的话,--daemon 可以省掉。
4. 状态查询
随便找一台客户端执行。
./acmedeliver-client -s wss://your-server:9443 -k 你的密码 --status在线的客户端、每个域名证书还剩几天、每台机器最近一次有没有收到,都能看到。

写在最后
我是个菜鸡,整个项目完全是 vibe coding 出来的,肯定还有不少问题和细节没处理好。再次感谢原作者七夏浅笑。