273 lines
5.7 KiB
Markdown
273 lines
5.7 KiB
Markdown
主服务(CertCenter)
|
||
|
||
负责 acme.sh 申请与续签、维护 version 文件、提供 HTTPS 下载接口,以及生成“拉取脚本”。
|
||
|
||
拉取脚本(deploy-cert.sh)
|
||
|
||
部署到每台服务器,通过 cron/systemd timer 定时执行:检查版本 → 下载 → 比对 → 原子替换 → nginx -t → reload。
|
||
|
||
这样每台业务服务器 不需要安装 Python、FastAPI、acme.sh 或任何 Agent,只需要一个 Shell 脚本和 cron。
|
||
|
||
### 最终架构
|
||
|
||
CertCenter
|
||
|
||
acme.sh · AliDNS · version · HTTPS 下载
|
||
|
||
HTTPS 拉取
|
||
|
||
VPS-A
|
||
|
||
blog.example.com
|
||
|
||
cron → deploy-cert.sh → reload nginx
|
||
|
||
VPS-B
|
||
|
||
api.example.com
|
||
|
||
cron → deploy-cert.sh → reload nginx
|
||
|
||
VPS-C
|
||
|
||
git.example.com
|
||
|
||
cron → deploy-cert.sh → reload nginx
|
||
|
||
### CertCenter 目录
|
||
|
||
```
|
||
certcenter/
|
||
├── app.py # FastAPI
|
||
├── config.yaml
|
||
├── certs/
|
||
│ └── example.com/
|
||
│ ├── fullchain.pem
|
||
│ ├── private.key
|
||
│ ├── version
|
||
│ └── metadata.json
|
||
└── templates/
|
||
└── deploy-cert.sh.j2 # 脚本模板
|
||
```
|
||
|
||
### 主服务提供的接口
|
||
|
||
| 接口 | 用途 |
|
||
| -------------------------------- | ---------------- |
|
||
| GET /api/version/{domain} | 返回当前版本号 |
|
||
| GET /api/cert/{domain}/fullchain | 下载 fullchain.pem |
|
||
| GET /api/cert/{domain}/private | 下载 private.key |
|
||
| GET /api/script/{domain} | 动态生成部署脚本 |
|
||
|
||
所有接口都使用:
|
||
|
||
http
|
||
|
||
```
|
||
Authorization: Bearer <TOKEN>
|
||
```
|
||
|
||
### 版本文件
|
||
|
||
`/srv/certs/example.com/version`
|
||
|
||
```
|
||
20260718143000
|
||
```
|
||
|
||
续签成功后,CertCenter 只需要:
|
||
|
||
Bash
|
||
|
||
```
|
||
date -u +%Y%m%d%H%M%S > /srv/certs/example.com/version
|
||
```
|
||
|
||
客户端先请求这个小文件,只有版本变化时才下载证书。
|
||
|
||
### 生成的拉取脚本(核心)
|
||
|
||
下面就是 CertCenter 返回给服务器的脚本内容。你只需要把它保存为 `/usr/local/bin/deploy-cert.sh`。
|
||
|
||
Edit
|
||
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
DOMAIN="example.com"
|
||
BASE_URL="[https://cert.example.com](https://cert.example.com)"
|
||
TOKEN="REPLACE_WITH_TOKEN"
|
||
|
||
CERT_DIR="/etc/nginx/ssl/example.com"
|
||
TMP_DIR="/tmp/cert-sync-${DOMAIN}"
|
||
VERSION_FILE="${CERT_DIR}/.version"
|
||
RELOAD_CMD="systemctl reload nginx"
|
||
|
||
mkdir -p "${CERT_DIR}" "${TMP_DIR}"
|
||
|
||
auth=(-H "Authorization: Bearer ${TOKEN}")
|
||
|
||
# 1. 获取远端版本
|
||
|
||
REMOTE_VERSION=$(curl -fsSL "${auth[@]}"
|
||
"${BASE_URL}/api/version/${DOMAIN}")
|
||
|
||
# 2. 获取本地版本
|
||
|
||
LOCAL_VERSION=""
|
||
if [[ -f "${VERSION_FILE}" ]]; then
|
||
LOCAL_VERSION=$(cat "${VERSION_FILE}")
|
||
fi
|
||
|
||
# 3. 版本一致则退出
|
||
|
||
if [[ "${REMOTE_VERSION}" == "${LOCAL_VERSION}" ]]; then
|
||
exit 0
|
||
fi
|
||
|
||
# 4. 下载新证书
|
||
|
||
curl -fsSL "${auth[@]}"
|
||
"${BASE_URL}/api/cert/${DOMAIN}/fullchain"
|
||
-o "${TMP_DIR}/fullchain.pem"
|
||
|
||
curl -fsSL "${auth[@]}"
|
||
"${BASE_URL}/api/cert/${DOMAIN}/private"
|
||
-o "${TMP_DIR}/private.key"
|
||
|
||
chmod 600 "${TMP_DIR}/private.key"
|
||
|
||
# 5. 备份旧证书
|
||
|
||
cp -f "${CERT_DIR}/fullchain.pem" "${CERT_DIR}/fullchain.pem.bak" 2>/dev/null || true
|
||
cp -f "${CERT_DIR}/private.key" "${CERT_DIR}/private.key.bak" 2>/dev/null || true
|
||
|
||
# 6. 原子替换
|
||
|
||
mv "${TMP_DIR}/fullchain.pem" "${CERT_DIR}/fullchain.pem"
|
||
mv "${TMP_DIR}/private.key" "${CERT_DIR}/private.key"
|
||
|
||
# 7. 校验 Nginx 配置
|
||
|
||
if nginx -t; then
|
||
echo "${REMOTE_VERSION}" > "${VERSION_FILE}"
|
||
${RELOAD_CMD}
|
||
echo "[$(date -Is)] certificate updated: ${REMOTE_VERSION}"
|
||
else
|
||
echo "Nginx config test failed, rollback" >&2
|
||
mv -f "${CERT_DIR}/fullchain.pem.bak" "${CERT_DIR}/fullchain.pem" 2>/dev/null || true
|
||
mv -f "${CERT_DIR}/private.key.bak" "${CERT_DIR}/private.key" 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
|
||
脚本已经包含:
|
||
|
||
* 版本检查(避免重复下载)
|
||
|
||
* 下载证书
|
||
|
||
* SHA/内容变化隐式通过版本号控制
|
||
|
||
* 原子替换(`mv`)
|
||
|
||
* `nginx -t` 校验
|
||
|
||
* 失败自动回滚
|
||
|
||
* 成功后 `systemctl reload nginx`
|
||
|
||
### CertCenter 的“生成脚本”接口
|
||
|
||
FastAPI 示例(核心逻辑):
|
||
|
||
Python
|
||
|
||
Run
|
||
|
||
```
|
||
from fastapi import FastAPI, HTTPException
|
||
from fastapi.responses import PlainTextResponse
|
||
from jinja2 import Environment, FileSystemLoader
|
||
import secrets
|
||
|
||
app = FastAPI()
|
||
env = Environment(loader=FileSystemLoader("templates"))
|
||
|
||
TOKENS = {
|
||
"server-a": "tok_xxx",
|
||
"server-b": "tok_yyy",
|
||
}
|
||
|
||
@app.get("/api/script/{domain}", response_class=PlainTextResponse)
|
||
def generate_script(domain: str, server: str):
|
||
token = TOKENS.get(server)
|
||
if not token:
|
||
raise HTTPException(404, "unknown server")
|
||
|
||
tpl = env.get_template("deploy-cert.sh.j2")
|
||
return tpl.render(
|
||
domain=domain,
|
||
base_url="https://cert.example.com",
|
||
token=token,
|
||
cert_dir=f"/etc/nginx/ssl/{domain}",
|
||
)
|
||
```
|
||
|
||
### 业务服务器首次部署
|
||
|
||
只需要执行一次:
|
||
|
||
Bash
|
||
|
||
```
|
||
curl -fsSL \
|
||
"https://cert.example.com/api/script/example.com?server=server-a" \
|
||
-o /usr/local/bin/deploy-cert.sh
|
||
|
||
chmod +x /usr/local/bin/deploy-cert.sh
|
||
```
|
||
|
||
然后添加 cron:
|
||
|
||
cron
|
||
|
||
```
|
||
*/30 * * * * /usr/local/bin/deploy-cert.sh >> /var/log/deploy-cert.log 2>&1
|
||
```
|
||
|
||
### 主服务续签流程
|
||
|
||
Bash
|
||
|
||
```
|
||
# 每天凌晨执行
|
||
/root/.acme.sh/acme.sh --cron
|
||
|
||
# 如果证书更新,安装到统一目录
|
||
/root/.acme.sh/acme.sh --install-cert \
|
||
-d example.com \
|
||
--key-file /srv/certs/example.com/private.key \
|
||
--fullchain-file /srv/certs/example.com/fullchain.pem
|
||
|
||
# 更新版本号
|
||
date -u +%Y%m%d%H%M%S > /srv/certs/example.com/version
|
||
```
|
||
|
||
### 为什么这个方案最适合你
|
||
|
||
极轻量
|
||
|
||
业务服务器只需要 Bash + curl + cron,没有常驻进程。
|
||
|
||
安全
|
||
|
||
AliDNS AccessKey 只保存在 CertCenter,业务服务器只拿下载 Token。
|
||
|
||
自动化
|
||
|
||
续签后客户端自动感知版本变化并更新证书。
|
||
|
||
易扩展
|
||
|
||
新增服务器只需执行一次 curl 下载脚本并添加 cron。
|