# 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.14(pydantic-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 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.top(CertCenter 自身的域名) 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] 配置 ACME(Staging 环境 + 阿里云 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 ` - 返回: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 ` - 参数: - `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)调试,确认无误后再切换到生产环境。