Files
acme-auto/README.md
T

884 lines
22 KiB
Markdown
Raw Normal View History

2026-07-18 20:09:26 +08:00
# CertCenter - 轻量化证书管理中心
一站式 SSL 证书管理平台,集成 ACME 证书申请、自动续签、多服务器分发,通过 Web UI 完成所有操作。
---
## 目录
- [架构概览](#架构概览)
- [环境要求](#环境要求)
- [安装部署](#安装部署)
- [WebUI 操作指南](#webui-操作指南)
- [仪表盘](#1-仪表盘)
- [服务器管理](#2-服务器管理)
- [域名管理](#3-域名管理)
- [ACME 配置](#4-acme-配置)
- [部署日志](#5-部署日志)
- [ACME 日志](#6-acme-日志)
- [测试验证](#测试验证)
- [环境检查](#1-环境检查)
- [端到端测试](#2-端到端测试)
- [客户端部署指南](#客户端部署指南)
- [Linux 服务器](#linux-服务器)
- [Windows 服务器](#windows-服务器)
- [API 参考](#api-参考)
- [客户端 API](#客户端-api)
- [管理 API](#管理-api)
- [配置说明](#配置说明)
- [常见问题](#常见问题)
---
## 架构概览
```
┌─────────────────────────────────────────────────────────┐
│ CertCenter 服务 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ WebUI │ │ ACME 引擎 │ │ 数据库 │ │
│ │ (Vue) │ │ (certbot) │ │ (SQLite) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────────────┴──────────────┘ │
│ │ │
│ FastAPI 后端服务 │
└──────────────────────┬──────────────────────────────────┘
┌────────────┼────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ VPS-A │ │ VPS-B │ │ VPS-C │
│ Linux │ │ Linux │ │ Windows │
│ cron │ │ cron │ │ schtasks│
└─────────┘ └─────────┘ └─────────┘
```
**核心流程:**
1. 在 WebUI 配置 ACME 凭据(阿里云 DNS / Cloudflare
2. 添加服务器和域名信息
3. 点击"申请证书"CertCenter 自动完成 DNS 验证并签发证书
4. 业务服务器通过 cron 定时拉取最新证书
5. 证书到期前自动续签
---
## 环境要求
### 服务端(CertCenter
| 依赖 | 版本 | 说明 |
|------|------|------|
| Python | 3.11 ~ 3.12 | ⚠️ 不支持 3.14pydantic-core 无预编译 wheel |
| certbot | 2.10+ | ACME 证书申请 |
| certbot-dns-alicloud | 最新 | 阿里云 DNS 插件(如使用阿里云) |
### 客户端(业务服务器)
| 依赖 | 说明 |
|------|------|
| bash / PowerShell | 执行部署脚本 |
| curl / Invoke-WebRequest | 下载证书 |
| cron / schtasks | 定时任务 |
---
## 安装部署
### 0. 确认 Python 版本
```bash
python --version
# 需要 3.11.x 或 3.12.x,不支持 3.14
```
如果版本不对,先装 Python 3.12
- 下载地址:https://www.python.org/downloads/release/python-3128/
- 安装时勾选 "Add python.exe to PATH"
### 1. 克隆项目
```bash
git clone <repo-url> certcenter
cd certcenter
```
### 2. 创建虚拟环境并安装依赖
```bash
# 创建虚拟环境(指定 Python 3.12
py -3.12 -m venv .venv
# 激活虚拟环境
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux/Mac
# 安装依赖
pip install -r requirements.txt
```
如果使用阿里云 DNS,还需要安装 certbot 插件:
```bash
pip install certbot-dns-alicloud
```
### 3. 安装前端依赖并构建
```bash
cd frontend
npm install
npm run build
cd ..
```
构建完成后,`frontend/dist/` 目录包含前端静态文件,FastAPI 会自动加载。
### 4. 配置环境变量
```bash
cp .env .env
```
编辑 `.env` 文件:
```ini
# 数据库路径(SQLite
DATABASE_URL=sqlite+aiosqlite:///./certcenter.db
# 证书存储目录
CERT_DIR=/srv/certs
# CertCenter 的访问地址(客户端通过此地址拉取证书)
BASE_URL=https://cert.example.com
```
### 5. 启动服务
```bash
python -m backend.main
```
服务默认监听 `http://0.0.0.0:8000`
**首次启动时,系统会自动为 CertCenter 的域名生成一份自签证书**(存放在 `CERT_DIR` 下),确保 HTTPS 可用。后续正式签发证书后会自动替换。
### 6. 引导流程(首次使用)
首次使用时的推荐操作顺序:
```
1. 启动 CertCenter(自签证书自动生成)
python -m backend.main
2. 打开 WebUI,配置 ACME 凭据(阿里云 AK/SK
3. 添加域名:cert.zhzp.topCertCenter 自身的域名)
4. 点击"申请证书",为 CertCenter 签发正式证书
5. 重启 CertCenter,正式证书自动加载
# 此时所有客户端连接都是受信任的 HTTPS
```
**为什么需要这个流程?**
```
CertCenter 使用 HTTPS → 需要证书 → 证书由 CertCenter 自己签发
这就是"引导"问题
```
解决方式:先用自签证书启动 → 签发正式证书 → 替换。客户端脚本已内置 `curl -k` 跳过 TLS 验证,不受影响。
### 7. 生产环境部署(可选)
使用 systemd 托管服务:
```bash
cat > /etc/systemd/system/certcenter.service << 'EOF'
[Unit]
Description=CertCenter Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/certcenter
ExecStart=/opt/certcenter/venv/bin/python -m backend.main
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now certcenter
```
如果需要 HTTPS 访问 CertCenter 本身,可以使用 nginx 反向代理:
```nginx
server {
listen 443 ssl;
server_name cert.example.com;
ssl_certificate /etc/nginx/ssl/cert.example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/cert.example.com/private.key;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
---
## 测试验证
部署完成后,建议按以下步骤验证系统是否正常工作。
### 1. 环境检查
运行部署验证脚本,检查所有依赖和配置:
```bash
python -m tests.test_setup
```
该脚本会检查:
| 检查项 | 说明 |
|--------|------|
| Python 依赖 | fastapi、sqlalchemy、acme、cryptography 等 |
| Certbot | certbot 命令是否可用 |
| DNS 插件 | certbot-dns-alicloud 或 certbot-dns-cloudflare |
| .env 配置 | 环境变量文件是否存在 |
| 数据库 | 数据库连接是否正常 |
| 前端构建 | frontend/dist/ 是否存在 |
| 证书目录 | 存储目录是否可写 |
| BASE_URL | 是否已修改默认值 |
| Let's Encrypt 连通性 | Staging 和 Production 环境是否可达 |
| FastAPI 服务 | 服务能否正常启动 |
所有检查通过后,进入下一步。
### 2. 端到端测试
使用 Let's Encrypt **Staging 环境**(测试环境)验证完整的证书签发流程。
**前提条件:**
- 一个使用阿里云 DNS 解析的域名
- 阿里云 RAM 子账号的 AccessKey(需有 `AliyunDNSFullAccess` 权限)
- CertCenter 服务已启动
**执行测试:**
```bash
python -m tests.test_e2e \
--domain test.example.com \
--email admin@example.com \
--ak YOUR_ACCESS_KEY \
--sk YOUR_ACCESS_SECRET
```
**测试流程:**
```
[0] 检查 API 服务是否正常
[1] 配置 ACMEStaging 环境 + 阿里云 DNS 凭据)
[2] 创建测试服务器
[3] 创建测试域名配置
[4] 申请证书(自动完成 DNS-01 验证)
[5] 检查证书信息(签发者、有效期、SAN)
[6] 测试版本接口
[7] 测试脚本生成
[8] 测试证书下载
```
**预期输出:**
```
==================================================
端到端测试: test.example.com
==================================================
✅ [0] API 服务正常
✅ [1] ACME 配置完成
✅ [2] 服务器 ID: 1
✅ [3] 域名 ID: 1
✅ [4] 证书申请成功: Certificate issued, expires: 2026-10-16 00:00:00
✅ [5] 签发者: CN=(STAGING) Artificial Apricot R3, O=Let's Encrypt
✅ [5] 有效期: 2026-07-18 ~ 2026-10-16
✅ [5] 域名: ['test.example.com']
✅ [5] 剩余天数: 90
✅ [6] 版本号: 1
✅ [7] 脚本生成成功,长度: 1234 字符
✅ [8] 证书下载成功
==================================================
✅ 端到端测试完成!
==================================================
证书位置: /tmp/cert-e2e-test/test.example.com/
Staging 证书不受浏览器信任,仅用于验证流程。
确认流程正常后,在 WebUI 中切换到生产环境重新签发。
```
**Staging vs Production**
| 环境 | 地址 | 说明 |
|------|------|------|
| Staging | `https://acme-staging-v02.api.letsencrypt.org/directory` | 测试环境,证书不受信任,无速率限制 |
| Production | `https://acme-v02.api.letsencrypt.org/directory` | 生产环境,正式证书,有速率限制 |
**建议:** 先用 Staging 环境完成全流程测试,确认无误后在 WebUI 中切换到 Production 环境,删除测试域名,重新添加正式域名并签发。
---
## WebUI 操作指南
启动服务后,浏览器访问 `http://服务器IP:8000`(或配置的域名)。
### 1. 仪表盘
首页展示:
- **统计卡片**:服务器总数、域名总数、即将过期证书数
- **最近部署日志**:最近 10 条客户端部署记录
- **快捷操作**:新增服务器、新增域名
### 2. 服务器管理
**新增服务器:**
1. 点击侧边栏"服务器管理"
2. 点击"+ 新增服务器"
3. 填写表单:
- **名称**:服务器标识,如 `vps-blog``api-server`
- **平台**:选择 `Linux``Windows`
- **Token**:点击"生成"按钮自动生成,或手动填写
- **IP**:可选,仅用于备注
4. 点击"创建"
**编辑/删除服务器:**
- 在列表中点击"编辑"修改信息
- 点击"删除"会同时删除该服务器下的所有域名配置
### 3. 域名管理
**新增域名:**
1. 点击侧边栏"域名管理"
2. 点击"+ 新增域名"
3. 填写表单:
- **所属服务器**:选择域名所在的业务服务器
- **域名**:如 `example.com``api.example.com`
- **证书存放路径**:证书在业务服务器上的存放目录
- Linux 示例:`/etc/nginx/ssl/example.com`
- Windows 示例:`C:\certs\example.com`
- **校验命令**:替换证书后验证服务是否正常的命令
- nginx 示例:`nginx -t`
- 其他服务根据实际情况填写
- **重载命令**:证书更新后重载服务的命令
- Linux 示例:`systemctl reload nginx`
- Windows 示例:`nginx -s reload`
4. 点击"创建"
**申请证书:**
1. 在域名列表中找到目标域名
2. 点击"申请证书"按钮
3. 系统自动完成:
- 通过 DNS-01 验证(自动添加 `_acme-challenge` TXT 记录)
- 向 Let's Encrypt 申请证书
- 保存证书到配置的目录
- 更新证书到期时间
4. 申请成功后,"证书到期"列会显示到期日期
**复制部署命令:**
点击"复制部署命令",获取在业务服务器上执行的一键部署命令。
**查看脚本:**
点击"查看脚本",预览将下发到业务服务器的部署脚本内容。
### 4. ACME 配置
**基本配置:**
1. 点击侧边栏"ACME 配置"
2. 配置项:
- **ACME 服务器**
- Let's Encrypt (生产):正式环境使用
- Let's Encrypt (测试):测试环境,不受速率限制
- **邮箱**Let's Encrypt 注册邮箱,用于接收证书到期提醒
- **DNS 提商**:选择使用的 DNS 服务商
- **续签提前天数**:证书到期前几天自动续签(默认 30 天)
**DNS 凭据配置:**
根据选择的 DNS 提商填写对应的凭据:
| DNS 提商 | 需要的凭据 |
|----------|-----------|
| 阿里云 DNS | Access Key + Access Secret |
| Cloudflare | API Token |
**获取阿里云 DNS 凭据:**
1. 登录 [阿里云控制台](https://ram.console.aliyun.com/)
2. 创建子账号,授予 `AliyunDNSFullAccess` 权限
3. 创建 AccessKey,记录 Access Key ID 和 Access Key Secret
**自动续签:**
点击"🔄 自动续签所有"按钮,系统会:
1. 检查所有域名的证书到期时间
2. 对即将过期的证书自动执行续签
3. 显示每个域名的续签结果
### 5. 部署日志
查看所有客户端(业务服务器)的证书拉取部署记录:
- **时间**:部署执行时间
- **服务器**:执行部署的服务器
- **域名**:部署的域名
- **状态**:成功 / 失败 / 跳过
- **消息**:详细信息
支持按状态筛选。
### 6. ACME 日志
查看所有证书申请/续签操作记录:
- **时间**:操作时间
- **域名**:操作的域名
- **操作**:申请 / 续签 / 吊销 / 注册
- **状态**:成功 / 失败 / 进行中
- **消息**:结果信息
- **详情**:点击"查看"显示完整日志
---
## 客户端部署指南
### Linux 服务器
**首次部署(只需执行一次):**
```bash
# 1. 下载部署脚本(从 WebUI "复制部署命令" 获取)
curl -fsSL "https://cert.example.com/api/script/example.com?server_name=vps-blog" \
-o /usr/local/bin/deploy-cert.sh
# 2. 添加执行权限
chmod +x /usr/local/bin/deploy-cert.sh
# 3. 添加 cron 定时任务(每 30 分钟检查一次)
echo '*/30 * * * * /usr/local/bin/deploy-cert.sh >> /var/log/deploy-cert.log 2>&1' | crontab -
```
**手动执行测试:**
```bash
/usr/local/bin/deploy-cert.sh
```
**查看部署日志:**
```bash
tail -f /var/log/deploy-cert.log
```
**脚本执行流程:**
```
1. 请求 CertCenter 获取远端版本号
2. 对比本地版本号
├── 一致 → 退出(无需更新)
└── 不一致 ↓
3. 下载新证书(fullchain.pem + private.key
4. 备份旧证书
5. 替换新证书
6. 执行校验命令(nginx -t
├── 失败 → 回滚旧证书 → 退出
└── 成功 ↓
7. 更新本地版本号
8. 重载服务(systemctl reload nginx
```
### Windows 服务器
**首次部署:**
```powershell
# 1. 创建脚本目录
New-Item -ItemType Directory -Force -Path C:\scripts
# 2. 下载部署脚本
Invoke-WebRequest `
-Uri "https://cert.example.com/api/script/www.example.com?server_name=win-web" `
-OutFile C:\scripts\deploy-cert.ps1
# 3. 添加计划任务(每 30 分钟)
schtasks /create /sc minute /mo 30 /tn "CertDeploy-www.example.com" /tr "powershell -ExecutionPolicy Bypass -File C:\scripts\deploy-cert.ps1"
```
**手动执行测试:**
```powershell
powershell -ExecutionPolicy Bypass -File C:\scripts\deploy-cert.ps1
```
---
## API 参考
### 客户端 API
供业务服务器上的部署脚本调用,使用 Bearer Token 认证。
#### 获取证书版本号
```
GET /api/version/{domain}
```
- 认证:无需认证
- 返回:版本号字符串(纯文本)
**示例:**
```bash
curl https://cert.example.com/api/version/example.com
# 返回: 3
```
#### 下载证书
```
GET /api/cert/{domain}/fullchain
GET /api/cert/{domain}/private
```
- 认证:`Authorization: Bearer <token>`
- 返回:PEM 格式证书文件
**示例:**
```bash
curl -H "Authorization: Bearer tok_xxx" \
https://cert.example.com/api/cert/example.com/fullchain \
-o fullchain.pem
```
#### 获取部署脚本
```
GET /api/script/{domain}?server_name={server_name}
```
- 认证:无需认证
- 返回:部署脚本内容(bash 或 PowerShell
**示例:**
```bash
curl https://cert.example.com/api/script/example.com?server_name=vps-blog \
-o /usr/local/bin/deploy-cert.sh
```
#### 上报部署结果
```
POST /api/report/{domain}?status={status}&message={message}
```
- 认证:`Authorization: Bearer <token>`
- 参数:
- `status``success` / `failed` / `skipped`
- `message`:可选,详细信息
**示例:**
```bash
curl -X POST \
-H "Authorization: Bearer tok_xxx" \
"https://cert.example.com/api/report/example.com?status=success&message=updated+to+v3"
```
### 管理 API
供 WebUI 前端调用,所有接口以 `/admin/api` 为前缀。
#### 统计数据
```
GET /admin/api/stats
```
返回:
```json
{
"server_count": 3,
"domain_count": 5,
"expiring_count": 1,
"recent_logs": [...]
}
```
#### 服务器管理
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/admin/api/servers` | 服务器列表 |
| GET | `/admin/api/servers/{id}` | 服务器详情 |
| POST | `/admin/api/servers` | 新增服务器 |
| PUT | `/admin/api/servers/{id}` | 编辑服务器 |
| DELETE | `/admin/api/servers/{id}` | 删除服务器 |
**新增/编辑请求体:**
```json
{
"name": "vps-blog",
"platform": "linux",
"token": "tok_xxxxxxxxxxxx",
"ip": "1.2.3.4"
}
```
#### 域名管理
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/admin/api/domains` | 域名列表 |
| GET | `/admin/api/domains/{id}` | 域名详情 |
| POST | `/admin/api/domains` | 新增域名 |
| PUT | `/admin/api/domains/{id}` | 编辑域名 |
| DELETE | `/admin/api/domains/{id}` | 删除域名 |
**新增/编辑请求体:**
```json
{
"server_id": 1,
"domain": "example.com",
"cert_dir": "/etc/nginx/ssl/example.com",
"check_cmd": "nginx -t",
"reload_cmd": "systemctl reload nginx"
}
```
#### ACME 配置
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/admin/api/acme/config` | 获取 ACME 配置 |
| PUT | `/admin/api/acme/config` | 更新 ACME 配置 |
**更新请求体:**
```json
{
"acme_server": "https://acme-v02.api.letsencrypt.org/directory",
"email": "admin@example.com",
"dns_provider": "aliyun",
"dns_credentials": "{\"access_key\":\"xxx\",\"access_secret\":\"xxx\"}",
"renew_days": 30
}
```
#### ACME 操作
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/admin/api/acme/issue/{domain_id}` | 申请证书 |
| POST | `/admin/api/acme/renew/{domain_id}` | 续签证书 |
| POST | `/admin/api/acme/auto-renew` | 自动续签所有 |
#### 日志查询
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/admin/api/logs?status={status}` | 部署日志 |
| GET | `/admin/api/acme/logs?status={status}` | ACME 日志 |
#### 证书信息
```
GET /admin/api/cert-info/{domain_id}
```
返回:
```json
{
"domain": "example.com",
"cert_info": {
"subject": "CN=example.com",
"issuer": "CN=R3, O=Let's Encrypt",
"not_before": "2026-01-01T00:00:00",
"not_after": "2026-04-01T00:00:00",
"serial_number": "1234567890",
"san": ["example.com"]
},
"need_renew": false,
"days_left": 75
}
```
---
## 配说说明
### 环境变量(.env
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `DATABASE_URL` | `sqlite+aiosqlite:///./certcenter.db` | 数据库连接字符串 |
| `CERT_DIR` | `/srv/certs` | 证书存储根目录 |
| `BASE_URL` | `https://cert.example.com` | CertCenter 访问地址 |
### 证书存储目录结构
```
/srv/certs/
├── example.com/
│ ├── fullchain.pem # 完整证书链
│ ├── private.key # 私钥
│ └── .version # 当前版本号
├── api.example.com/
│ ├── fullchain.pem
│ ├── private.key
│ └── .version
└── .certbot-work/ # certbot 工作目录
├── .certbot-config/
└── .certbot-logs/
```
### 版本号说明
版本号为递增整数,初始值为 `0`。每次证书申请/续签成功后自动 +1。
客户端通过对比版本号判断是否需要下载新证书,避免重复下载。
---
## 常见问题
### Q: 申请证书时报错 "DNS problem: NXDOMAIN looking up TXT for _acme-challenge.xxx"
**原因:** DNS TXT 记录未正确添加或未及时传播。
**解决方案:**
1. 确认阿里云 DNS 凭据正确(Access Key / Secret
2. 确认域名使用的是阿里云 DNS 服务
3. 等待 DNS 记录传播(通常 1-2 分钟)
4. 使用测试环境(Let's Encrypt Staging)调试
### Q: 客户端脚本执行后没有更新证书
**可能原因:**
1. 版本号一致,无需更新(正常行为)
2. Token 不正确,API 返回 401
3. 网络不通,无法访问 CertCenter
**排查步骤:**
```bash
# 1. 手动测试版本接口
curl https://cert.example.com/api/version/example.com
# 2. 手动测试证书下载
curl -H "Authorization: Bearer tok_xxx" \
https://cert.example.com/api/cert/example.com/fullchain
# 3. 查看脚本详细执行过程
bash -x /usr/local/bin/deploy-cert.sh
```
### Q: nginx -t 校验失败,证书已回滚
**原因:** 新证书与 nginx 配置不匹配。
**排查步骤:**
```bash
# 1. 检查 nginx 配置
nginx -t
# 2. 检查证书内容
openssl x509 -in /etc/nginx/ssl/example.com/fullchain.pem -text -noout
# 3. 检查私钥是否匹配
openssl x509 -noout -modulus -in /etc/nginx/ssl/example.com/fullchain.pem | md5sum
openssl rsa -noout -modulus -in /etc/nginx/ssl/example.com/private.key | md5sum
```
### Q: 如何更换 DNS 提商
1. 在 WebUI 的"ACME 配置"页面修改"DNS 提商"下拉框
2. 填写新提商的凭据
3. 点击"保存凭据"
4. 重新申请证书
### Q: 如何备份和迁移
**备份:**
```bash
# 备份数据库
cp certcenter.db certcenter.db.bak
# 备份证书目录
tar czf certs-backup.tar.gz /srv/certs/
# 备份配置
cp .env .env.bak
```
**迁移:**
1. 在新服务器部署项目
2. 恢复数据库和证书目录
3. 修改 `.env` 中的 `BASE_URL` 为新地址
4. 更新业务服务器脚本中的地址(或重新执行部署命令)
### Q: 如何添加通配符证书
通配符证书需要 DNS 验证,CertCenter 已支持。在域名管理中添加:
```
域名: *.example.com
```
申请时系统会自动通过 DNS-01 验证完成签发。
### Q: Let's Encrypt 速率限制
Let's Encrypt 有以下限制:
| 限制类型 | 限制值 |
|----------|--------|
| 每个域名每周证书数 | 50 张 |
| 每个域名每小时重复证书数 | 5 张 |
| 注册账户数 | 每个 IP 每小时 10 个 |
**建议:** 先使用测试环境(Staging)调试,确认无误后再切换到生产环境。