一、1Panel 部署器三个缺陷(其中两个此前完全不可见)
1. `POST /websites/{id}/https` 的字段名是 `websiteSSLId`,不是 `sslId`。
发 `sslId` 会被 Go 静默忽略成零值 0,于是
`websiteSSLRepo.GetFirst(WithByID(0))` → 返回
`HTTP 200 + code 500「服务错误: record not found」`,
报错文案落在 DB 层,完全指不到参数名 —— 整个 t-t.live 部署被这条卡住。
对照实验:`sslId=13 → 500` / `websiteSSLId=13 → 200 code=200`。
2. 换证书内容的接口选错。`POST /websites/ssl/update` 的结构体
`WebsiteSSLUpdate` **根本没有** `certificate` / `privateKey` 字段,
传了被丢弃、且 `domains` 只从 `otherDomains` 取(不传就清空),
还会顺带把 `autoRenew` 置 false —— 而它**照样返回 200 success**。
实测:原样 update 后 5 个站点的 `ssl/*.pem` mtime+md5 一个都没变。
正确接口是 `POST /websites/ssl/upload` + `sslID > 0`:取记录 → 覆盖
→ 重算 ExpireDate/domains → `UpdateSSLConfig()` → 重新物化站点文件。
副作用:`Upload()` 把 `primaryDomain` 重算成证书第一个 SAN
(#11 因此从 `usj.cc` 漂成 `*.usj.cc`)→ 必须补 `domains` 兜底匹配,
否则每次续期都新建一条重复记录。
3. `deploy()` 幂等捷径漏了「记录被换过」这一维:`sslId` 没变但内容变了时
会跳过绑定,站点文件就停留在旧证书。改为引入 `replaced` 标志强制重绑。
二、另外两处静默失败
4. `parsePemInfo()` 对**完整链**返回 `{}`:旧实现把 PEM 各段 base64 拼接后
一次 `atob`,中间段尾部的 `=` 填充导致抛错,整函数返回空 →
`rec.expireAt` 退化成「签发时刻 + 90 天」。而完整链恰恰是部署器最常
拿到的形态。改为只解析第一段(叶证书)。
顺带新增 `derLen()` / `parseSanFromDer()`,精确定位 SAN 扩展
OID `2.5.29.17` 再读 `[2] dNSName`,替掉原来的字节扫描启发式。
这条同时是「多吉云复用失效」的根因:判据缺到期时间,只看域名集合
就永远认为已覆盖 → 续期静默空转。修好后判据带上 `notAfter` 比对(1 天容差)。
5. DNS-01 挑战通知会撞 `400 authorization must be pending`(200181.xyz 连中两次)。
这是**竞态**不是逻辑错:「先读状态再 POST」挡不住毫秒级窗口。
已在 POST 侧做幂等容错(只认这一句),最终由 `pollAuthz` 定论。
三、Worker 退回只读
- `crons` 去掉 `10 4 * * *`,`index.ts` 里 renew 分支整体删除
- `POST /ssl/issue` 改 **501 硬拒绝**(而不是静默降级),响应给出国内机命令
- 签发 + 部署整条链路跑在国内机容器 `cn-certkeeper`
四、顺带修掉的两个「配置被悄悄抹掉」
- `configSave()` 不再丢掉表单不管理的 `probe_connect` / `probe_sni`。
之前管理员在面板改任何一项,这两个字段就会被清空,
后果是挂在 CDN 后的 t-t.live 探针退回公网、被误判成「还剩 80 多天」,
源站证书到期也不续。现在保存时从旧配置带过来。
五、新增 4 个常驻运维工具(deploy/cn-certkeeper/src/)
- `renew-one.mjs` 只对单个域名签发+部署(原 `/renew` 无域名过滤,会全量重签)
- `redeploy.mjs` 复用已签好的证书只重跑部署(不碰 ACME,不白烧配额)
- `rollback-dogecloud.mjs` 应急把 CDN 域名绑回指定证书 id
- `txt-inspect.mjs` `_acme-challenge` 下的 TXT 残留盘点/清理
- `selfcheck-acme.mjs` CA 层诊断:只读目录 + 复用账户,不签发不部署
六、测试与文档
- 自测新增 [11] 节 8 项 PEM 解析回归(样本是 openssl 现场生成、内联写死的
叶+中间证书,两段都以 `=` 结尾,正是 bug 现场),含精确值断言:
> 125 项通过,0 失败
- `npm run typecheck` 零错误
- 方案文档:§9.11 由「待验证」改为定案(`ssl/update` 不物化、
`ssl/upload+sslID` 才物化);新增 §9.12「本轮又修掉的 5 个静默失败」、
§9.13「本轮最终状态」、§9.14「certimate 工作流清查」
线上验收:三个域名(t-t.live / usj.cc / 200181.xyz)线上证书均为
LiteSSL ECC、2027-01-04 到期、daysLeft=90、needRenew=false;
1Panel 证书库 5 条精简为 3 条且全部在用;
certimate 停掉全部「会签发并部署」的工作流(团团 / 优世界 / 200181.xyz),
保留三条纯监控告警。
71 lines
3.1 KiB
JavaScript
71 lines
3.1 KiB
JavaScript
/**
|
||
* 证书管家的**域名配置唯一事实源**。
|
||
*
|
||
* 为什么单独抽出来(2026-10-06):
|
||
* 这份配置有两个消费方 ——
|
||
* ① `seed-ssl-config.mjs`:写进 Cloudflare KV(Worker 侧 UI / 探针 / 自检用)
|
||
* ② `export-certkeeper-data.mjs`:生成国内机 cn-certkeeper 的数据目录
|
||
* 原先 CONFIG 内联在 seed 脚本里,而那个脚本**一被 import 就执行**
|
||
* (读 .env、连 KV 开始写),第 ② 个消费方没法安全复用。
|
||
* 抽出来之后两边共用一份定义,不会出现「改了 KV 忘了改国内机」。
|
||
*
|
||
* ★ 站点名必须与 1Panel 里的 `primaryDomain` 或 `alias` **精确对上**,
|
||
* 否则部署时会被跳过。下面这些是 2026-10-06 从面板实测出来的。
|
||
*/
|
||
|
||
export const CONFIG = {
|
||
version: 1,
|
||
notify: { emails: ['177018615@qq.com'], daysBefore: 30 },
|
||
domains: [
|
||
{
|
||
name: 'usj.cc',
|
||
san: ['usj.cc', '*.usj.cc'],
|
||
dns: 'tencent-usj',
|
||
deploy: ['dogecloud', '1panel'],
|
||
// ★ 站点名必须与 1Panel 里的 `primaryDomain` 或 `alias` 精确对上,
|
||
// 否则部署时会被跳过。下面这些是 2026-10-06 从面板实测出来的
|
||
// (面板上**没有** primaryDomain 为 `usj.cc` 的网站 —— 它只是证书名)。
|
||
dogecloud_domains: ['usj.cc', 'www.usj.cc', 'artalk.usj.cc'],
|
||
one_panel_sites: [
|
||
'artalk.usj.cc', // blog 评论后端
|
||
'openlist.usj.cc', // 网盘
|
||
'wifi.usj.cc',
|
||
'openwrt.usj.cc',
|
||
'vw.usj.cc', // vaultwarden(alias 才是这个名字)
|
||
],
|
||
// ★ 源站直连探针:usj.cc 走多吉云 CDN,公网握手拿到的是 CDN 边缘证书;
|
||
// 要续的是本机 1Panel 上那张。面板里没有 usj.cc 同名站点,
|
||
// 所以 SNI 指到真正引用该证书的 artalk.usj.cc。
|
||
probe_connect: '119.29.215.187',
|
||
probe_sni: 'artalk.usj.cc',
|
||
disabled: false,
|
||
},
|
||
{
|
||
name: 't-t.live',
|
||
san: ['t-t.live', '*.t-t.live'],
|
||
dns: 'tencent-tt',
|
||
deploy: ['1panel'],
|
||
one_panel_sites: ['t-t.live', 'www.t-t.live', 'pl.t-t.live', 'pwd.t-t.live', 'certd.t-t.live'],
|
||
// ★★ t-t.live 的公网入口是**腾讯云 EO 边缘加速**(apex 是一条 CNAME →
|
||
// t-t.live.eo.dnse2.com),公网握手量到的是 EO 的边缘证书(LE,12-27),
|
||
// 而我们要续的是本机 nginx 上那张源站证书(曾停在 07-11、10-09 到期)。
|
||
// 不连源站 IP 的话,续期判定会永远「还剩 80 多天」→ 源站证书悄悄过期。
|
||
probe_connect: '119.29.215.187',
|
||
probe_sni: 't-t.live',
|
||
disabled: false,
|
||
},
|
||
{
|
||
name: '200181.xyz',
|
||
san: ['200181.xyz', '*.200181.xyz'],
|
||
dns: 'cloudflare',
|
||
deploy: ['1panel'],
|
||
one_panel_sites: ['ssh.200181.xyz'],
|
||
// 同理:200181.xyz 的 NS 在 Cloudflare(带代理),公网拿到的是 CF 边缘证书。
|
||
// 面板上的站点名是 ssh.200181.xyz,用它当 SNI。
|
||
probe_connect: '119.29.215.187',
|
||
probe_sni: 'ssh.200181.xyz',
|
||
disabled: false,
|
||
},
|
||
],
|
||
};
|