通知(P0-1): - 邮件:自写 SMTP 客户端,零依赖。465 隐式 TLS / 587·25 STARTTLS、 AUTH LOGIN+PLAIN、RFC2047 中文主题、base64 正文、多收件人; 服务器不支持 STARTTLS 时默认拒绝明文(需显式 allowInsecure) - MeoW:按官方文档实现,昵称寻址、msgType/htmlHeight 走查询参数、 以响应体 status 判定成功(HTTP 恒 200)、429/404/403 可读化、 本地限流器遵守 3s/15min/60h - 告警:连续 N 次失败去抖 + 恢复通知 + 同轮合并 + 去抖状态持久化 (data/notify-state.json,gitignore)+ 静默时段(跨零点) - --test-notify 自检命令、--data-dir / SHIROKO_DATA_DIR 证书(P0-2): - 独立 TLS 握手取 notAfter(不动既有 fetch 探测路径),默认 6 小时一次 - 面板徽标 ≤14 天黄 / ≤7 天红,阈值 14/7/3 各告一次、已过期只报最严重一档 - 单个目标可用 tls: false 关闭 事件与维护(P1-1 的后端部分): - data/events.json 追加式事件流(最近 500 条)+ GET /api/events + 快照内附最近 20 条;redactDetails 时脱敏 - 维护窗口:窗口内不计入可用率、不发告警、显示「维护中」徽标 测试抓到并修复的三个真 bug: 1. SMTP 空闲超时无人处理 → 服务器不响应会把探针循环挂死 2. MeoW 限流器把空历史当 0 → 第一条通知永远被自己拦掉 3. 去抖状态在无事轮次不落盘 → 连续失败计数丢失、永远达不到阈值 验证:新增 verify-notify.cjs 39 项(假 SMTP / 假 MeoW / 三轮集成 / 维护窗口 / 真实证书握手),六套合计 154 项全通过。
156 lines
8.6 KiB
Markdown
156 lines
8.6 KiB
Markdown
# 安全说明:刷新滥用防护、SSH 探测与信息泄露控制
|
||
|
||
面向「公网可访问的状态页」这个前提编写。核心结论先给:
|
||
|
||
| 场景 | 建议 |
|
||
| --- | --- |
|
||
| 本地 / 内网自用 | 默认配置即可,无需额外改动 |
|
||
| 公网发布 | 静态快照(GitHub Pages / EdgeOne)或 Cloudflare Worker + `REFRESH_TOKEN`;**不要把 Node 探针直接暴露到公网** |
|
||
| 目标含内网资产 | 用 `local.monitors.json` 分层,公开配置只留 `id/name/group`,并开 `hideTargets` + `redactDetails` |
|
||
|
||
---
|
||
|
||
## 一、`/api/refresh` 的滥用风险与分层防护
|
||
|
||
「刷新即重新检测」意味着**一个未鉴权的 GET 请求会触发一次对所有目标的出站连接**。滥用的三种形态:
|
||
|
||
1. **探测放大器**:任何人反复调用,把本服务当作对目标站点施压的跳板(借刀杀人)
|
||
2. **资源耗尽**:每次强制刷新都会对所有目标发起连接,慢目标(如超时 10s)会长时间占用并发
|
||
3. **目标配额消耗**:被诱导高频探测的第三方目标可能限流甚至封禁我们的出口 IP(真实案例:GitHub 未认证 API 上限 60 次/小时,被高频探测打满后返回 403,面板如实记为离线)
|
||
|
||
### 防护措施(已实现)
|
||
|
||
| 位置 | 措施 | 默认 |
|
||
| --- | --- | --- |
|
||
| Node 探针 | 仅允许内网/回环来源触发;**只信任 socket 对端地址,不解析 `X-Forwarded-For`**(该头部可被伪造) | 开启 |
|
||
| Node 探针 | 每来源冷却 `settings.refreshCooldownSec`,超限返回 `429` + `Retry-After` | 5s |
|
||
| Node 探针 | 轮次去重:多标签页/多客户端共享同一轮探测;2s 内的重复请求直接复用快照 | 开启 |
|
||
| Node 探针 | 单轮上限 22s 兜底(`FORCED_MAX_WAIT_MS`),防止卡死连接长期占用请求 | 开启 |
|
||
| Node 探针 | 总开关 `settings.allowRefresh: false`,完全关闭强制探测(只读模式) | 可配 |
|
||
| Node 探针 | 拒绝响应只回错误码(`{"error":"refresh-throttled"}`),不回显目标与内部细节 | 固定 |
|
||
| Worker | **必须配置 `REFRESH_TOKEN` 才开放** `/api/refresh`,否则一律 403;token 比较用等长逐字节异或,避免按字符提前返回的时序差异 | 默认关闭 |
|
||
| 面板 | 收到 401/403 自动退回只读轮询(页脚标注「只读」);429 仅本轮降级并在按钮提示 | 自动 |
|
||
|
||
### 配置参考
|
||
|
||
```jsonc
|
||
{
|
||
"settings": {
|
||
"refreshInterval": 30, // 面板刷新节奏
|
||
"probeOnRefresh": true, // 刷新是否重新探测
|
||
"allowRefresh": true, // false = 完全关闭强制探测(公网部署建议)
|
||
"refreshLocalOnly": true, // 仅内网/回环来源可触发
|
||
"refreshCooldownSec": 5 // 每来源冷却
|
||
}
|
||
}
|
||
```
|
||
|
||
单个目标也可退出「刷新即检测」,避免被高频探测压垮配额:
|
||
|
||
```jsonc
|
||
{ "id": "github-api", "interval": 300, "probeOnRefresh": false }
|
||
```
|
||
|
||
### 已知限制(请按需叠加外部措施)
|
||
|
||
- **限流在进程内**:多个探针实例不共享计数;需要强约束时请在网关(Nginx / Cloudflare Rate Limiting)再加一层
|
||
- **反向代理场景**:因为不信任 `X-Forwarded-For`,前置代理时所有请求都来自代理的内网地址,`refreshLocalOnly` 会失效——此时应设 `allowRefresh: false`,或自行在代理层做认证与限流
|
||
- Worker 的同隔离体去重是 best-effort(不同边缘节点各自计数)
|
||
|
||
---
|
||
|
||
## 二、SSH 这类服务怎么探测
|
||
|
||
支持三种探测类型,按「侵入性从低到高」:
|
||
|
||
| 类型 | 做什么 | 适合 |
|
||
| --- | --- | --- |
|
||
| `tcp` | 仅建立 TCP 连接,测连通性与连接耗时 | 任意端口存活监控 |
|
||
| `banner` | 连接后**被动读取服务端主动发送的标识串**,可与规则比对 | SSH / SMTP / FTP / POP3 / IMAP / HTTP 等主动问候的协议 |
|
||
| `http` | 完整 HTTP 请求,读状态码 | Web 服务 |
|
||
|
||
### SSH 配置示例
|
||
|
||
```jsonc
|
||
{
|
||
"id": "ssh-gateway",
|
||
"name": "跳板机 SSH",
|
||
"group": "基础设施",
|
||
"type": "banner",
|
||
"host": "10.0.0.5", // 真实地址建议放 local.monitors.json
|
||
"port": 22,
|
||
"timeout": 5,
|
||
"interval": 300,
|
||
"expectBanner": "^SSH-2\\.0-", // 策略:必须是 SSH 2.0
|
||
"revealBanner": false // 默认不回显标识串(含软件版本)
|
||
}
|
||
```
|
||
|
||
`expectBanner` 让探测同时具备**合规检查**能力:例如要求 `^SSH-2\.0-OpenSSH_[89]`,若目标降级到旧版本,探测即判为离线并触发面板告警。
|
||
|
||
### 明确不做的三件事
|
||
|
||
1. **不做 SSH 登录/认证尝试**:会触发目标侧 fail2ban/sshguard 封禁,需要凭据,还会在目标留下审计噪音
|
||
2. **不发送任何应用层数据**:因此需要客户端先行发包的协议(Redis 的 `PING`、MySQL 握手)暂不适用——这是刻意保留的安全边界,不是能力缺失
|
||
3. **不在标识串不匹配时回显实际收到的内容**:避免把「目标是什么版本」写进公开的 `status.json`
|
||
|
||
banner 探测的副作用仅有一次 TCP 连接与读取,与端口扫描器不同:不产生登录失败记录,也不改动目标任何状态。
|
||
|
||
---
|
||
|
||
## 三、信息泄露控制
|
||
|
||
状态页天生是公开的,泄露面有三处,逐个封堵:
|
||
|
||
| 泄露面 | 风险 | 措施 |
|
||
| --- | --- | --- |
|
||
| `public/monitors.json` | 面板必需文件,静态部署必然公开,含真实 URL / 内网主机端口 | **私有配置分层**:公开文件只写 `id/name/group`,真实地址放仓库根的 `local.monitors.json`(已 gitignore),探针按 `id` 合并覆盖 |
|
||
| 通知凭据 | SMTP 密码、MeoW 昵称(等同密钥)泄露 → 被冒名发信/推送 | 与地址一样只放 `local.monitors.json` 或环境变量;`public/monitors.json` 中 `notify.channels.*` 保持 `enabled: false` |
|
||
| `status.json` / `/api/status` | 探针输出,Pages 部署会公开,`detail` 可能含地址、版本、错误码 | `settings.redactDetails: true` → detail 只保留「正常/异常」;TCP 详情已默认不含地址;UptimeRobot 镜像目标的地址在 `hideTargets` 开启时不下发;事件时间线在 `redactDetails` 下只保留类型与时间 |
|
||
| 面板渲染 | 直接把地址、错误详情展示给访客 | `settings.hideTargets: true` → 不渲染任何地址(含 tooltip),显示「已隐藏」 |
|
||
| UptimeRobot API Key | 泄露后可读取(只读 Key 不能改配置,但会暴露监控清单) | 只放 `local.monitors.json`(gitignore)或环境变量 `UPTIMEROBOT_API_KEY`;`public/monitors.json` 里 `apiKey` 必须留空 |
|
||
| `/api/v2/getMonitors`(兼容接口) | 无鉴权,暴露全部目标与可用率 | 内容与公开面板一致,故默认开放;如需收敛请在其前方加带认证的代理,或设 `allowRefresh: false` 并只暴露静态快照 |
|
||
|
||
### 对外发布状态页的推荐组合
|
||
|
||
```jsonc
|
||
// local.monitors.json(不入库,仅本机/CI 环境存在)
|
||
{
|
||
"monitors": [
|
||
{ "id": "blog", "url": "https://blog.example.com" },
|
||
{ "id": "nas", "host": "192.168.1.10", "port": 5000 }
|
||
]
|
||
}
|
||
```
|
||
|
||
```jsonc
|
||
// public/monitors.json(公开)
|
||
{
|
||
"settings": { "hideTargets": true, "redactDetails": true },
|
||
"monitors": [
|
||
{ "id": "blog", "name": "博客站点", "group": "网站", "type": "http", "interval": 60, "timeout": 10 },
|
||
{ "id": "nas", "name": "家庭存储", "group": "基础设施", "type": "tcp", "interval": 300, "timeout": 5 }
|
||
]
|
||
}
|
||
```
|
||
|
||
这样即使 `status.json` 与面板完全公开,访客也只能看到「有哪些服务、是否在线、延迟、可用率」。
|
||
|
||
补充:`dataFilter`(`both` / `config` / `uptimerobot`)只决定**展示与快照输出**哪些目标,不影响探测行为;如果希望某个来源的数据完全不进入公开快照,应使用 `config` 过滤或把对应目标从配置中移除。
|
||
|
||
### 仍然会被公开的信息(请自行评估)
|
||
|
||
- **服务数量与名称**:命名不要包含敏感语义(不要叫「内网财务系统」)
|
||
- **延迟特征**:多次采样可反推大致网络位置
|
||
- **可用率与故障时间**:这本身就是状态页要公开的信息
|
||
- **GitHub Actions 场景**:CI 读不到未提交的 `local.monitors.json`,需用 repository secrets 在 workflow 中生成,或只监控公开目标
|
||
|
||
---
|
||
|
||
## 四、其他基线
|
||
|
||
- `.gitignore` 已包含 `local.monitors.json` 与 `.dev.vars`(Cloudflare 本地密钥)
|
||
- Worker 的 `REFRESH_TOKEN` 用 `wrangler secret put REFRESH_TOKEN` 管理,不要写进 `wrangler.toml`
|
||
- 面板为只读前端:无 cookie、无写入接口、无服务端会话;渲染配置字段时统一做 HTML 转义(`escapeHtml`),XSS 面仅剩配置自身
|
||
- Node 探针同时充当静态服务器,**不要直接暴露公网**;确实需要时在其前方加带认证的反向代理,并设 `allowRefresh: false`
|