// 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 ( "strings" "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: // 往假后台里填账号密码是证据最强的行为,抓到一次就足够。 // 纯路径探测不走这个阈值(见 route.honeypotHandler.Serve)。 DefaultHoneypotThreshold = 1 // DefaultHoneypotBanMinutes 蜜罐封禁的默认时长(分钟)。 // // 刻意远长于常规 ban_duration_minutes(15 分钟):蜜罐命中的不是「访问了一个 // 可疑路径」,而是「往假后台里填了账号密码」—— 对方已经付出破解成本、 // 且拿到了一次响应(知道「后台存在」)。15 分钟不足以让对方放弃, // 24 小时能覆盖一轮扫描活动周期。 DefaultHoneypotBanMinutes = 1440 // DefaultHoneypotCredDir 凭据取证文件的默认目录(相对进程工作目录)。 // // 不放在数据库所在目录:这两者的权限、备份策略、留存周期都应该分开管。 DefaultHoneypotCredDir = "data/honeypot-creds" // DefaultIPGeoProvider 默认的地理定位数据源:auto(ip-api 打底 + 腾讯精修)。 DefaultIPGeoProvider = "auto" // DefaultHoneypotReportDir 安全汇总报告文件的默认目录。 // // 与凭据目录分开:报告里有攻击者 IP 明细,但它是可以外发/归档的材料; // 凭据目录里是明文账号密码,永不外发。放在一起容易误打包。 DefaultHoneypotReportDir = "data/reports" ) // 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 } // 封禁时长:缺省 24 小时(见 DefaultHoneypotBanMinutes 的理由)。 // 显式写 <= 0 是逃生口:交给 autoban 回退到 ban_duration_minutes, // 但那样「蜜罐按常规时长封」的意图不明显,故明确告警。 if !sec.HasKey("honeypot_ban_minutes") { Security.HoneypotBanMinutes = DefaultHoneypotBanMinutes } else if Security.HoneypotBanMinutes <= 0 { logrus.WithField("fallback_key", "ban_duration_minutes"). Warn("honeypot_ban_minutes <= 0 已忽略,蜜罐封禁将回退为 ban_duration_minutes") } // 凭据文件目录:未配置或留空都用默认值。 // 这里不回退「不保存」——用户显式开启蜜罐就是想要取证, // 目录写错会在 route 层触发「不可用 → 不保存凭据」的告警,而不是静默丢弃。 if strings.TrimSpace(Security.HoneypotCredDir) == "" { Security.HoneypotCredDir = DefaultHoneypotCredDir } // 地理定位数据源:未配置 → auto。 // 显式写了不认识的值时保持原样,由 geoip 层解析并告警 —— // 在配置层悄悄改成 auto 会让人以为配置生效了。 if strings.TrimSpace(Security.IPGeoProvider) == "" { Security.IPGeoProvider = DefaultIPGeoProvider } // 周期报告:三个开关都用 HasKey 判定缺省值(同 HoneypotAutoClean 的理由)。 if !sec.HasKey("honeypot_report_enabled") { Security.HoneypotReportEnabled = true } if !sec.HasKey("honeypot_report_weekly") { Security.HoneypotReportWeekly = true } if !sec.HasKey("honeypot_report_monthly") { Security.HoneypotReportMonthly = true } if strings.TrimSpace(Security.HoneypotReportDir) == "" { Security.HoneypotReportDir = DefaultHoneypotReportDir } }