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。
|