Files
tamabox/internal/conf/honeypot_defaults.go
T
tamakyi d300167a30 fix(conf): 配置键名容错 + 未知键告警 + 蜜罐保留期缺省值修正
现场故障:配置写 `honeypot_retention_days <= 30`,日志却报
「蜜罐自动清理未启用:honeypot_retention_days <= 0」。

根因(实测 ini v1.67.0 行为确认):
1. 键名多一个 t(retention vs retain)→ ini 对未知键**静默忽略**,保留期成 0;
2. `<= 30` 这种写法让 ini 把键名解析成 "honeypot_retention_days <"
   (带尾随比较符),更加匹配不上;
3. 警告文案里写的也是错键名,等于在误导运维往错的键上配。

修复(三层):
- 键名归一化(internal/conf/keynorm.go):修剪首尾空白、剥掉尾随 `<` `>`
  `=` 等比较符残留,并按别名表纠正常见误拼。归一化结果必须命中该段
  合法键名白名单才会被采用 —— 不认识的键不猜,交给未知键告警。
- 未知键告警(internal/conf/unknown.go):启动时逐段比对键名,无法识别的
  键 WARN 出来并给出最接近的合法键名建议。合法键名由反射从结构体 ini tag
  得出(单一事实来源,新增配置项自动纳入,不会脱节)。
- 缺省值语义修正(internal/conf/honeypot_defaults.go):
  * 保留期缺省 30 天(原先零值 0 = 永不清理);显式 <= 0 也回退 30。
    要关闭自动清理请用 honeypot_auto_clean = false。
  * honeypot_auto_clean 缺省 true。原先靠 bool 零值,ini 的 bool 解析是
    「有键才赋值」,未配置与显式关闭无法区分 → 老配置升级上来自动清理
    实际是关着的。现改用 HasKey 判定。
  * honeypot_max_rows 缺省 200000;显式 0 = 不限(会告警提醒只增不减)。
  * honeypot_threshold 缺省 1。

实测 ini 行为(探针程序,已删):
- 键缺失或值非法时 MapTo **不覆盖**预置默认值 → 可安全预置缺省值;
- 键名大小写敏感(Honeypot_Auto_Clean 不生效);
- HasKey 能区分「缺失」与「显式 0」;
- 段不存在时 MapTo 返回 nil 且不修改目标。

顺带清理 app.sample.ini 里的两条死配置(app.title / server.xsrf_expire):
代码中从未读取(站点标题走 i18n),留着会让新检测对官方模板误报。

验证:
- 用现场原始配置端到端复现 → 归一化后保留期 30 天、自动清理开启、上限 20 万,
  日志明确说明「配置键名已自动纠正」;
- 误拼键(honeypot_autoclean / honeypot_max_row)自动纠正;未知键(totally_bogus)
  WARN 告警;
- 显式 honeypot_auto_clean = false 被尊重为关闭;完全未配置时缺省开启;
- 新增 TestSampleIniHasNoUnknownKeys 回归检测:官方样例配置零误报;
- go build ./... + go test -timeout 600s ./... 全绿,go vet 无输出。
2026-10-21 13:41:19 +08:00

82 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 tamakyi. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package conf
import (
"github.com/sirupsen/logrus"
)
// 蜜罐相关配置的默认值。
//
// 为什么要显式兜底:ini 的 MapTo 在「键缺失」或「值非法」时**不覆盖**目标字段,
// 所以结构体的零值就是「未配置」的表现。而零值对这几个开关恰好是最危险的那个:
//
// - HoneypotAutoClean 零值 false → 未配置的站点自动清理是关的,
// honeypot_hits 只增不减;
// - HoneypotRetainDays 零值 0 → 保留期 0 天 = 永不清理;
// - HoneypotMaxRows 零值 0 → 无行数上限。
//
// 现场就踩过这个坑:配置写成了 honeypot_retention_days(多一个 t),
// 键名不匹配被 ini 静默忽略,保留期成了 0,自动清理形同关闭。
const (
// DefaultHoneypotRetainDays 蜜罐记录默认保留天数。
DefaultHoneypotRetainDays = 30
// DefaultHoneypotMaxRows 蜜罐记录默认行数上限。
// 实测单行约 281 字节(含索引,常规 UA),20 万行约 53 MB。
DefaultHoneypotMaxRows = 200000
// DefaultHoneypotThreshold 蜜罐命中多少次即封禁。默认 1(一次即封):
// 蜜罐路径与真实业务路由零交集,正常访客不可能命中。
DefaultHoneypotThreshold = 1
)
// sanitizeHoneypot 归一化蜜罐相关配置,修正「未配置」与「显式关闭」的歧义。
//
// 语义约定(两个开关各管一件事,不再互相牵连):
//
// honeypot_auto_clean 是否启用后台自动清理。缺省 true(开)。
// honeypot_retain_days 保留多少天。缺省 30 天。
//
// 也就是说:**要关闭自动清理请写 honeypot_auto_clean = false**,
// 而不要靠把 retain_days 写成 0 —— 保留 0 天意味着「记录一进来就删」,
// 既不是任何人的真实意图,也会让「自动清理到底开没开」变得无法判断。
//
// 显式写 honeypot_max_rows = 0 仍表示「不设行数上限」(这是明确的逃生口),
// 但会告警提醒:没有时间闸也没有数量闸的表只增不减。
func sanitizeHoneypot() {
sec := File.Section("security")
hasRetain := sec.HasKey("honeypot_retain_days")
hasMaxRows := sec.HasKey("honeypot_max_rows")
// 保留期:未配置或显式 <= 0 → 回退默认。
// 显式 0 的语义是「立刻删除」,与「不自动清理」混淆,判为误配。
if !hasRetain {
Security.HoneypotRetainDays = DefaultHoneypotRetainDays
} else if Security.HoneypotRetainDays <= 0 {
logrus.WithField("default", DefaultHoneypotRetainDays).
Warn("honeypot_retain_days <= 0 已被忽略,改用默认值(如需关闭自动清理请设 honeypot_auto_clean = false)")
Security.HoneypotRetainDays = DefaultHoneypotRetainDays
}
// 行数上限:未配置 → 默认;显式 0 → 不设上限(逃生口,但告警)。
if !hasMaxRows {
Security.HoneypotMaxRows = DefaultHoneypotMaxRows
} else if Security.HoneypotMaxRows <= 0 {
logrus.Warn("honeypot_max_rows = 0 表示不设行数上限:蜜罐表将只增不减,请确认这是有意为之")
}
// 自动清理:缺省开启。
// 判据用 HasKey 而不是值 —— 因为 ini 对 bool 的解析是「有键才赋值」,
// 缺键时零值 false 会让「未配置」与「显式关闭」无法区分。
// 显式 false 是合法的关闭意图,予以尊重(与 retain_days 的处理不同)。
if !sec.HasKey("honeypot_auto_clean") {
Security.HoneypotAutoClean = true
}
// 封禁阈值:缺省 1。
if !sec.HasKey("honeypot_threshold") {
Security.HoneypotThreshold = DefaultHoneypotThreshold
}
}