一、封禁链路整条没接上(P0,已实测复现)
route.New 的 autoban.New 漏传 HoneypotEnabled/Threshold/Duration,而
RecordHoneypot 首行就是 `if !b.honeypotEnabled { return false }`
→ 蜜罐命中从不封禁、落库 banned 恒 false、IsBanned 永不为真。
同一批改动又把中间件里蜜罐路径的常规计数也跳过了,净效果是蜜罐路径
从「10 次封禁」退化为零计数零封禁 —— 是 regression 不是修复。
处理:抽 newBanner() 作为唯一装配入口并补齐参数;新增 banner_config_test
四个用例(全部经由 newBanner,不再自建 Banner)。变异验证:分别把
HoneypotEnabled 置 false、HoneypotThreshold 置 0,对应用例按预期失败。
二、诱捕语义(审计时漏掉,比上面更影响功能价值)
原实现对**所有方法**计数且阈值 1 → 扫描器 GET 一次假登录页就吃 403,
永远走不到「POST 提交账号密码」那一步,蜜罐的核心价值(拿到字典)归零。
处理:提交凭据 → RecordHoneypot(不受 enable_auto_ban 约束,立即封);
纯探测 → 常规 Record(阈值 10)。新增 TestHoneypotProbeDoesNotBanImmediately,
变异验证:把 GET 也塞进 RecordHoneypot,测试按预期失败。
三、落库边界(实测取证,非推断)
- Path 原样落库而列宽 512 → PG `SQLSTATE 22001 值太长了(512)`,整条证据丢失;
实测 600 字符失败、500 字符成功;
- 三处按字节截断 → `SQLSTATE 22021 无效的 "UTF8" 编码字节顺序`(中文密码/URL
可稳定触发)。统一改 rune 截断并按列宽逐项对齐。
四、资源与噪音
- alerted 无界 map → 带 TTL 的 deduper(上限 2 万,过期清理 + 超限硬裁),
同时承担告警 24h 冷却与纯探测落库采样(每 IP 每分钟一条)——
GET 洪泛不再变成等量 INSERT;
- credentialKeys 去掉 id/name,普通 JSON 探测不再被误判为凭据提交并触发假告警;
- canary 从 meta generator 移到 HTML 注释,generator 伪装成 WordPress,
不再同时向扫描器自曝「这是蜜罐」和「这站是 TamaBox」;
- serveLoginFailure 复用 WriteLoginPage,不再手抄响应头(已开始漂移);
- Describer 死代码 → Prefixes(),接到新命令 honeypot paths;
- honeypot_paths 解析挪到 honeypot.SplitPaths(CLI 与 route 共用),
补前导斜杠、拒绝 "/"(会让所有 404 变假登录页)。
五、凭据外置:数据库不再存用户名/密码
明文账号密码一旦入库就会跟着进备份、主从复制与任何能连库的报表工具,
事后无法彻底清除。改为:
- 新增 honeypot.CredRecord/CredStore:按天文件 creds-YYYY-MM-DD.jsonl,
目录 0700、文件 0600、坏行跳过、PurgeBefore 只删整天过期的文件;
- db.HoneypotHit 删掉 Username/Password/Fields/RawBody,只留 HadFields 元数据;
- HoneypotStore 删掉 TopCredentials/ListSinceByIP(CLI 改读文件后零调用);
- 新增 LegacyCredentialColumns/PurgeLegacyCredentialColumns:AutoMigrate 不删
既有列,老库升级后历史明文仍在 → 启动 WARN + `honeypot purge-creds` 显式 DROP;
- 清理器 HoneypotRetention 增加 CredPurgeable 参数,与 DB 记录共用保留期;
honeypot prune 也一并删过期凭据文件;
- CLI:hits 不再连库(DB 挂了也能查),stats 的凭据排行读文件;
- 配置 honeypot_cred_dir(缺省 data/honeypot-creds),.gitignore 加 /data/。
验证:go build ./... / go vet ./internal/... / go test ./...(含 PostgreSQL)全绿;
新增 credstore_test(追加/倒序/IP 过滤/坏行/Top/Purge/权限)、
TestHoneypotLegacyCredentialColumns(ADD COLUMN 模拟老库 → 检测 → DROP)、
TestHoneypotWritesCredentialsToFile;CLI 端到端手动验证 hits/--ip/--show-password。
198 lines
10 KiB
Go
198 lines
10 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(一次即封):
|
||
// 往假后台里填账号密码是证据最强的行为,抓到一次就足够。
|
||
// ⚠️ 它不作用于纯路径探测(GET 看一眼假登录页)—— 那类请求走常规
|
||
// ban_threshold_sensitive,否则扫描器 GET 一次就被封,永远走不到
|
||
// POST 提交凭据那一步,蜜罐也就拿不到字典。
|
||
// 缺省回退 1(见 sanitizeHoneypot)。
|
||
HoneypotThreshold int `ini:"honeypot_threshold"`
|
||
// HoneypotBanMinutes 蜜罐封禁时长(分钟)。缺省 1440(24 小时),
|
||
// 刻意远长于 ban_duration_minutes:命中蜜罐意味着对方已在提交账号密码,
|
||
// 证据强度高于单纯路径探测(见 DefaultHoneypotBanMinutes)。
|
||
// 显式 <= 0 表示回退 ban_duration_minutes。
|
||
HoneypotBanMinutes int `ini:"honeypot_ban_minutes"`
|
||
// HoneypotCredDir 凭据取证文件的存放目录。
|
||
//
|
||
// ⚠️ 捕获到的用户名/密码**不入库**,只追加到这个目录下的按天文件
|
||
// (creds-YYYY-MM-DD.jsonl,一行一条 JSON)。原因:明文账号密码一旦入库,
|
||
// 就会跟着进备份、主从复制和任何能连库的报表工具,事后无法彻底清除。
|
||
// 目录会被强制为 0700、文件 0600;留空使用默认 data/honeypot-creds。
|
||
// 目录不可写时只降级为「不保存凭据」,不会影响站点启动。
|
||
HoneypotCredDir string `ini:"honeypot_cred_dir"`
|
||
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"` // 仅内网自签名后端需要
|
||
}
|
||
)
|