现场故障:配置写 `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 无输出。
184 lines
9.2 KiB
Go
184 lines
9.2 KiB
Go
// Copyright 2022 E99p1ant. 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
|
||
|
||
// ⚠️ 仅应由 "-ldflags" 注入。
|
||
var (
|
||
BuildTime string
|
||
BuildCommit = "dev"
|
||
|
||
// StaticVersion 静态资源缓存版本号:base.css 与主题 css 共用,改样式只改这一处。
|
||
StaticVersion = "20261020g"
|
||
)
|
||
|
||
// 文本审核通道取值([security] text_censor_source)。定义在 conf 侧,
|
||
// 让配置校验(conf.Init)与通道装配(internal/security/censor)共用同一份字面量。
|
||
const (
|
||
CensorSourceAuto = "auto" // 七牛优先、阿里云兜底(历史行为,留空同义)
|
||
CensorSourceQiniu = "qiniu"
|
||
CensorSourceAliyun = "aliyun"
|
||
)
|
||
|
||
var (
|
||
App struct {
|
||
Production bool `ini:"production"`
|
||
ExternalURL string `ini:"external_url"`
|
||
ICP string `ini:"icp"`
|
||
DefaultLang string `ini:"default_lang"` // 站点默认语言(zh-CN/zh-TW/en/ja),留空回退简体中文
|
||
UptraceDSN string `ini:"uptrace_dsn"`
|
||
QiniuAccessKey string `ini:"qiniu_access_key"`
|
||
QiniuAccessSecret string `ini:"qiniu_access_secret"`
|
||
AliyunAccessKey string `ini:"aliyun_access_key"`
|
||
AliyunAccessKeySecret string `ini:"aliyun_access_key_secret"`
|
||
SentryDSN string `ini:"sentry_dsn"`
|
||
MaintenanceMode bool `ini:"maintenance_mode"`
|
||
ShareBackground string `ini:"share_background"`
|
||
FontPath string `ini:"font_path"`
|
||
}
|
||
|
||
// Theme 主题系统([theme] 段)。
|
||
// default :站点默认主题 slug,留空/非法回退内置 default(= 现在的外观)。
|
||
// available :启用哪些主题(逗号分隔),同时是箱主设置页的白名单;
|
||
// 列了但缺少 css 文件的会被忽略并告警(见 internal/theme)。
|
||
Theme struct {
|
||
Default string `ini:"default"`
|
||
Available string `ini:"available"`
|
||
}
|
||
|
||
Security struct {
|
||
EnableTextCensor bool `ini:"enable_text_censor"`
|
||
IPBlacklist string `ini:"ip_blacklist"`
|
||
IPBlacklistRedirectURL string `ini:"ip_blacklist_redirect_url"`
|
||
// TrustProxy 为 true 时信任 CDN 权威客户端头 CF-Connecting-IP / EO-Client-IP
|
||
// (以及受信代理设置的 X-Real-IP / X-Forwarded-For);否则一律用 RemoteAddr,
|
||
// 防止伪造 header 绕过速率限制与 IP 黑名单。
|
||
TrustProxy bool `ini:"trust_proxy"`
|
||
// TrustedProxies 逗号分隔的 IP / CIDR,仅限自己的反向代理 / LB。仅当 TCP 对端
|
||
// 落在该清单内才解析 X-Forwarded-For / X-Real-IP;留空则这两个头永不被信任
|
||
// (CDN 权威头仍会采用,前提是源站已由 Cloudflare Tunnel / 回源网段锁定,
|
||
// 从而免于维护上游 CDN 的庞大 CIDR 清单)。
|
||
TrustedProxies string `ini:"trusted_proxies"`
|
||
EnableAutoBan bool `ini:"enable_auto_ban"` // fail2ban 风格:检测到扫描特征后临时封禁来源 IP
|
||
// BanTrackNotFound 是否把未注册路径的 404 计入封禁统计。默认 false:仅命中后台
|
||
// 扫描特征路径(KindSensitive)才统计,避免共享出口(CDN 边缘 / CGNAT)下
|
||
// 正常用户被 404 探测流量连坐误伤。
|
||
BanTrackNotFound bool `ini:"ban_track_notfound"`
|
||
BanThresholdSensitive int `ini:"ban_threshold_sensitive"` // 窗口内敏感路径命中次数
|
||
BanThreshold404 int `ini:"ban_threshold_404"` // 窗口内 404 次数
|
||
BanWindowSeconds int `ini:"ban_window_seconds"` // 统计窗口(秒)
|
||
BanDurationMinutes int `ini:"ban_duration_minutes"` // 封禁时长(分钟)
|
||
// DefaultModerationStrictness 站点默认审核严格度(lax / standard / strict),
|
||
// 仅对未单独设置档位的账号生效(见 moderation.StrictnessFor)。留空或非法值回退
|
||
// strict(最严档),保证升级不会放宽拦截;默认拦截面可能较升级前略有扩大,
|
||
// 希望更少误伤则显式写 standard。
|
||
DefaultModerationStrictness string `ini:"default_moderation_strictness"`
|
||
// TextCensorSource 文本审核通道:qiniu / aliyun(内容安全 2.0 TextModeration,
|
||
// 个人实名账号即可开通)/ auto(默认,留空同义:七牛优先、阿里云复核)。
|
||
// 仅显式选定的通道会被调用,缺凭据时启动即报错(见 conf.Init)——
|
||
// 审核失败是 fail-closed(拒绝提交),会让发布与私信整站不可用且现象隐蔽。
|
||
TextCensorSource string `ini:"text_censor_source"`
|
||
// EnableHoneypot 开启「虚假后台登录界面」蜜罐:扫描器探测后台路径时
|
||
// 返回一个仿 WordPress 的假登录页,并记录其提交的用户名/密码等破解信息。
|
||
// 仅在「无任何路由匹配」的路径上生效,与真实业务路由零交集。
|
||
EnableHoneypot bool `ini:"enable_honeypot"`
|
||
// HoneypotPaths 追加自定义诱饵路径(逗号分隔),与内置清单并集。
|
||
HoneypotPaths string `ini:"honeypot_paths"`
|
||
// HoneypotRetainDays 蜜罐命中记录的保留天数,到期自动清理。
|
||
// ⚠️ 键名是 honeypot_retain_days(没有 tion)。写成 honeypot_retention_days
|
||
// 会被 ini 静默忽略 —— 现场踩过这个坑,表现为「自动清理未启用」。
|
||
// 缺省 30 天(见 sanitizeHoneypot);显式 <= 0 也回退 30。
|
||
// 要关闭自动清理请用 honeypot_auto_clean = false。
|
||
HoneypotRetainDays int `ini:"honeypot_retain_days"`
|
||
// HoneypotAutoClean 是否启用蜜罐记录的后台自动保留期清理。
|
||
// 缺省 true:honeypot_hits 是只追加的审计表,写入完全由外部扫描流量驱动,
|
||
// 没有自动清理的话表体积只增不减(这是「数据库被扫描撑爆」的真实路径)。
|
||
// 关闭后仍可手动执行 TamaBox honeypot prune。
|
||
// ⚠️ 缺省值靠 HasKey 判定(见 sanitizeHoneypot):ini 的 bool 解析是
|
||
// 「有键才赋值」,若只看零值就无法区分「未配置」与「显式关闭」。
|
||
HoneypotAutoClean bool `ini:"honeypot_auto_clean"`
|
||
// HoneypotMaxRows 蜜罐命中记录的行数上限。缺省 200000(约 53 MB)。
|
||
// 显式写 0 表示不设上限(逃生口,会告警提醒表只增不减)。
|
||
// 保留期是时间维度的上界,但攻击者可以用海量源 IP 在保留期内把表撑爆,
|
||
// 这是第二道闸。实测单行约 281 字节(含索引,常规 UA),20 万行约 53 MB。
|
||
HoneypotMaxRows int `ini:"honeypot_max_rows"`
|
||
// HoneypotThreshold 窗口内蜜罐命中多少次即封禁。默认 1(一次即封):
|
||
// 蜜罐路径与真实业务路由零交集,正常访客不可能命中,一次即足以判定扫描器。
|
||
// 缺省回退 1(见 sanitizeHoneypot)。
|
||
HoneypotThreshold int `ini:"honeypot_threshold"`
|
||
// HoneypotBanMinutes 蜜罐封禁时长(分钟)。留空则复用 ban_duration_minutes。
|
||
HoneypotBanMinutes int `ini:"honeypot_ban_minutes"`
|
||
SecurityAlertEmail string `ini:"security_alert_email"` // 触发封禁时告警收件人(留空不发送)
|
||
}
|
||
|
||
Server struct {
|
||
Port int `ini:"port"`
|
||
Listen string `ini:"listen"` // 绑定监听地址(含端口)。留空则默认 ":port"(监听所有网卡)。
|
||
Salt string `ini:"salt"`
|
||
XSRFKey string `ini:"xsrf_key"`
|
||
}
|
||
|
||
Database struct {
|
||
DSN string
|
||
|
||
Type string `ini:"type"`
|
||
User string `ini:"user"`
|
||
Password string `ini:"password"`
|
||
Host string `ini:"host"`
|
||
Port uint `ini:"port"`
|
||
Name string `ini:"name"`
|
||
Schema string `ini:"schema"`
|
||
DedupeUniqueFields bool `ini:"dedupe_unique_fields"`
|
||
}
|
||
|
||
Redis struct {
|
||
Addr string `ini:"addr"`
|
||
Password string `ini:"password"`
|
||
}
|
||
|
||
Recaptcha struct {
|
||
CaptchaType string `ini:"captcha_type"`
|
||
Domain string `ini:"domain"`
|
||
SiteKey string `ini:"site_key"`
|
||
ServerKey string `ini:"server_key"`
|
||
TurnstileStyle bool `ini:"turnstile_style"`
|
||
}
|
||
|
||
Pixel struct {
|
||
Host string `ini:"host"`
|
||
}
|
||
|
||
Upload struct {
|
||
DefaultAvatarURL string `ini:"default_avatar"`
|
||
DefaultBackground string `ini:"default_background"`
|
||
DefaultBackgroundImageURL string `ini:"default_qrbackgroundimage"`
|
||
AliyunEndpoint string `ini:"aliyun_endpoint"`
|
||
AliyunAccessID string `ini:"aliyun_access_id"`
|
||
AliyunAccessSecret string `ini:"aliyun_access_secret"`
|
||
AliyunBucket string `ini:"aliyun_bucket"`
|
||
AliyunBucketCDNHost string `ini:"aliyun_bucket_cdn_host"`
|
||
ImageEndpoint string `ini:"image_endpoint"`
|
||
ImageAccessID string `ini:"image_access_id"`
|
||
ImageAccessSecret string `ini:"image_access_secret"`
|
||
ImageBucket string `ini:"image_bucket"`
|
||
ImageBucketCDNHost string `ini:"image_bucket_cdn_host"`
|
||
}
|
||
|
||
Mail struct {
|
||
Account string `ini:"account"`
|
||
Password string `ini:"password"`
|
||
Port int `ini:"port"`
|
||
SMTP string `ini:"smtp"`
|
||
SkipTLSVerify bool `ini:"skip_tls_verify"` // 仅自签名 / 内网 SMTP 需要
|
||
}
|
||
|
||
Service struct {
|
||
Backends []struct {
|
||
Prefix string `ini:"prefix"`
|
||
ForwardURL string `ini:"forward_url"`
|
||
}
|
||
TLSInsecureSkipVerify bool `ini:"tls_insecure_skip_verify"` // 仅内网自签名后端需要
|
||
}
|
||
)
|