feat(security): v0.3.0 阶段2 收口健康与指标端点
CI / build-and-test (push) Canceled after 0s

收口三个匿名可读端点:

- 根路径 /metrics:默认完全不注册(访问 404);配置 METRICS_ALLOW_CIDR 后按内网白名单开放
- /health:改为鉴权(原本匿名泄漏版本、commit、运行时长、内存、goroutine 与配置错误文本)
- /dashboard:改为鉴权(原本匿名,且它同时聚合 /health 与 /metrics,是单点泄漏最严重的端点)
- /api/setup/status:改为鉴权(原本 normal 模式下仍匿名返回 installed/mode)

新增 /healthz 匿名存活探针,只回 200 与字面量 ok、不含任何字段,解决
"给 /health 加鉴权后 K8s 探针与 Docker HEALTHCHECK 会一律 401 导致 Pod 反复重启" 的问题;
新增 /api/admin/health 鉴权版详细健康数据,与 /api/admin/metrics 风格一致。

/dashboard 对浏览器 HTML 请求做内容协商:返回页面外壳由前端显示登录视图(SPA 登录态存在
sessionStorage、不随请求发送,服务端无从判断,强行 401 会把现有体验变成直接报错);
非 HTML 请求(脚本、抓取)无凭证一律 401。页面外壳本身不含数据,数据全部来自鉴权后的 API。

IP 白名单中间件复用 GetClientIP(已处理 XFF 与 TRUSTED_PROXY_HOPS),非白名单返回 404
而非 403,避免向扫描者确认端点存在。main.go 启动日志同步改为指向 /healthz 与 /api/admin/health。

验证(真实服务器端到端,非仅单元测试):
- 安装前 /healthz=200、/health=401
- 安装后未带凭证:/healthz=200 /health=401 /metrics=404 /dashboard=401 /api/setup/status=401
- /dashboard 带 Accept: text/html 得 200 页面外壳,Accept: application/json 得 401
- /healthz 响应体确认为字面量 ok(不含任何字段)
- 回退模式(未配 admin_key):业务 key 访问管理接口全部 200,向后兼容成立
- 配置独立 admin_key 后:业务 key 访问 /health /api/admin/* /api/voices /api/settings 全部 401,
  admin_key 全部 200,而 /v1/audio/speech 不受影响;GET /api/settings 只回打码值

另新增 middleware/metricsip_test.go 覆盖 CIDR 命中、越界、非法 IP、空列表拒绝,
以及伪造 X-Forwarded-For 不能绕过白名单。

go build ./... / go vet ./... / go test ./... -count=1 全绿。
This commit is contained in:
sun
2026-10-04 00:45:59 +08:00
parent d22e5f0801
commit 753de8ca9f
6 changed files with 339 additions and 18 deletions
+9 -2
View File
@@ -35,6 +35,8 @@ func main() {
setting.InitAllConfigs()
metrics.Init()
middleware.InitRateLimiter()
// v0.3.0:根路径 /metrics 的内网白名单(未配置则根路径完全不注册)
middleware.InitMetricsAllowList()
// 2) 启动期关键步骤:打开/建库 → 检测 lock → 判定模式
dbPath := ttsDBPath()
@@ -128,8 +130,13 @@ func main() {
log.Printf("OpenAI TTS endpoint: http://localhost:%s/v1/audio/speech", setting.Server.Port)
log.Printf("Admin WebUI: http://localhost:%s/admin", setting.Server.Port)
}
log.Printf("Health check: http://localhost:%s/health", setting.Server.Port)
log.Printf("Metrics: http://localhost:%s/metrics", setting.Server.Port)
log.Printf("Health check(匿名存活探针): http://localhost:%s/healthz", setting.Server.Port)
if middleware.MetricsAllowListConfigured() {
log.Printf("Metrics(内网白名单): http://localhost:%s/metrics", setting.Server.Port)
} else {
log.Printf("Metrics: 根路径 /metrics 未注册(未配置 METRICS_ALLOW_CIDR);请用鉴权版 /api/admin/metrics")
}
log.Printf("详细健康数据(鉴权): http://localhost:%s/api/admin/health", setting.Server.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)