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 无输出。
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user