diff --git a/.workbuddy/memory/2026-10-21.md b/.workbuddy/memory/2026-10-21.md index 1481b63..491ce8c 100644 --- a/.workbuddy/memory/2026-10-21.md +++ b/.workbuddy/memory/2026-10-21.md @@ -104,3 +104,53 @@ - 本轮修复需**重新部署**才在测试站生效;部署后 `curl` 一次 `/wp-login.php` 应 200、再一次 403。 - 需确认测试环境 `conf/app.ini` 的 `[security] enable_auto_ban`(零封禁直接原因)+ `enable_honeypot = true`。 - 无法 SSH(`100.79.185.45:19999` 全部账号 Permission denied),落库验证需用户执行 `TamaBox honeypot hits --since 24h --show-password`。 + +--- + +## 蜜罐表体积实测 + 自动清理(commit 63e851d,已 push) + +用户问「这些记录会不会导致数据库很臃肿」。**不靠推断,本地 PostgreSQL 建同构表灌仿真数据实测。** + +### 实测数字 +- 典型行 **244 字节**(heap 201 + 索引 43),10.5 万行 = 24 MB;常规 UA 下实测 281 B/行。 +- 字段均值:path 13B、user_agent 111B(单行最大头)、fields 3B、raw_body 3B。 +- `raw_body` 被 `maxCredentialFieldLen = 512` 截断,故字段数量随之受限(200 字段场景 rawBody 仍 512B)。 +- 最坏情况(手工构造 50 字段 × 500B)约 2375 B/行。 +- **UA 截短收益**:截到 100 省 1129 kB / 截到 64 省 4824 kB(10 万行)→ 仅 5–20%, + 远小于自动清理的确定性上界且损失取证信息 → **决定不做**。 + +### PostgreSQL 死元组语义(本轮最关键认知) +- `DELETE` 只标记死元组,**表文件不会变小**;空间需 VACUUM 后才可被后续 INSERT 复用。 +- 对照(10 万行,每轮删 25% 插 25%,行数恒定):有 VACUUM 22→24 MB **收敛**;无 VACUUM 22→52 MB **膨胀**。 +- ⚠️ **VACUUM 不缩小文件,只让空间可复用**。二次实测(5 万行、连续 4 轮删 12000 插 12000): + 行数恒定 5 万、文件稳定 15 MB 不再增长 —— 这才是要的效果(止住增长,不是让文件变小)。 + 想让文件立刻变小只有 `VACUUM FULL`(重建整表 + 锁表),蜜罐表万级行没必要。 +- ⚠️ `VACUUM` **不能在事务块内执行**(SQLSTATE 25001),也不能在 PL/pgSQL 函数体内执行(VACUUM 无法从函数执行)。 + GORM `db.Exec()` 走 `callbacks.Raw()` 处理器、**不包事务** → `WithContext(ctx).Exec("VACUUM ...")` 可用(已实测)。 +- PG18 已移除 `pg_stat_user_tables.n_tup_vacuum`,改用 `last_vacuum` 验证。 + +### 索引实测(EXPLAIN 驱动决策) +- `idx_honeypot_hits_provider`:10 万行 648 kB,**全代码库零查询使用**(TopSources 用 max(provider) 随 group by 顺带取)→ 已删。 +- `idx_honeypot_hits_created_at`:EXPLAIN 显示 `Index Scan Backward`,被 `ListSince` 实际使用 → 保留。 +- `idx_honeypot_hits_ip`:4.7 MB(最大),但 GROUP BY ip 走 Seq Scan + HashAggregate;**保留**(`ListSinceByIP` 需它)。 +- ⚠️ **GORM 去掉 struct tag 的 `index` 不会删除既有库上的旧索引** → 需显式 `HasIndex` + `DropIndex`(已加 `dropUnusedHoneypotIndexes`,启动时跑,失败仅告警)。 + +### 落地改动 +- `internal/db/honeypot_retention.go`(新):`HoneypotRetention` 后台清理器,每小时一轮,两道闸 + (时间 `honeypot_retain_days` + 数量 `honeypot_max_rows`)→ 清理后调 `Compact()`。 + 保留期 <=0 视为不自动清理(只告警);**`runOnce` 开头必须再挡一次 `retainDays<=0`**, + 否则 before=now 会把历史全删光(「保留 0 天」被误解释成「只留此刻」)。 +- `internal/db/honeypot.go`(新方法):`Compact()`(PG `VACUUM (ANALYZE)`)、`DeleteOldestBeyond()` + (跨方言通用,子查询按 id 倒序取 keepRows 边界)、`ListSinceByIP()`、`Size()`。 +- `internal/cmd/web.go`:接入清理器,`stopRetention` 用 defer close。 +- `internal/cmd/honeypot.go`:新增 `honeypot size` 子命令(行数/磁盘占用/时间跨度 + 与两道闸对照)。 +- 修 CLI `hits --ip` 真 bug:原先取回最近 N 条再在 Go 侧筛 → 指定 IP 的历史命中基本查不出来 → 改 SQL 层过滤。 +- 新配置:`honeypot_auto_clean`(默认 true)、`honeypot_max_rows`(默认 200000 ≈ 53 MB)。 + +### 踩坑 +- ⚠️ 测试预期别自己推算错:`DeleteBefore` 是**严格小于**(`created_at < before`),边界那条不删。 +- ⚠️ Python 补丁脚本里写 Go struct tag 的反引号 → bash 命令替换报错,**必须写成 .py 文件执行**(不能 `python -c`)。 +- ⚠️ `psql` 传中文 SQL 报编码错 → 用英文输出串或 `PGCLIENTENCODING=UTF8`。 +- ⚠️ 库名带连字符要用双引号:`CREATE DATABASE "tamabox-cli-test"`。 +- ⚠️ Edit 工具改 CRLF 文件会保持 CRLF,但**改写时我打的字会转码损坏**(出现过「实测指事/程定/不图」)→ 改完必须逐行读回复核。 +- 仓库内统一存 LF,工作区 CRLF 由 `core.autocrlf=true` 自动转换 —— 提交时 warning 属正常,不是行尾被改。 diff --git a/.workbuddy/memory/topics/review.md b/.workbuddy/memory/topics/review.md index 86f074e..1588a66 100644 --- a/.workbuddy/memory/topics/review.md +++ b/.workbuddy/memory/topics/review.md @@ -45,3 +45,17 @@ - `RecordHoneypot` 的键必须与 `Record` 一致(同样 `shared.LimitKey` 归一化),否则 `IsBanned` 判不出来。 - 备份/配置/密钥泄露扫描(`/backup.zip`、`/db.sql`、`/id_rsa.pem`)文件名主体随机,**只有后缀稳定** → 用 `path.Base()` 后缀匹配表,不能只靠子串(`"/.zip"` 匹配不到 `/backup.zip`)。 + +### 蜜罐表体积与自动清理(2026-10-21) +- `honeypot_hits` 是**只追加**审计表,写入完全由外部扫描流量驱动(不可预期、不可节流) + → 必须有自动保留期清理,否则表体积只增不减。 +- 两道闸:时间(`honeypot_retain_days`)+ 数量(`honeypot_max_rows`)。数量闸不可省 —— + 攻击者可用海量源 IP 在保留期内把表撑爆。 +- ⚠️ PG 的 `DELETE` 只标记死元组,**表文件不会变小**;无 VACUUM 时表体积单调膨胀 + (实测 22→52 MB)。清理后必须 `VACUUM (ANALYZE)`,但注意 **VACUUM 不缩小文件、 + 只让空间可复用**(止住增长即可,不必 `VACUUM FULL`)。 +- ⚠️ `VACUUM` 不能在事务块/函数体内执行 → GORM 走 `Exec()`(Raw 处理器,不包事务)可行。 +- ⚠️ 清理器的 `runOnce` 必须自己再挡一次 `retainDays<=0`,否则「保留 0 天」= 全删光。 +- 索引:GORM 去掉 struct tag 的 `index` **不会**删既有库上的旧索引 → 需显式 DropIndex。 + 删索引前先用 EXPLAIN 确认零查询使用(`provider` 索引实测 10 万行 648 kB 且零使用)。 +- CLI `honeypot size` 报告行数/磁盘占用/时间跨度 —— 把「有没有臃肿」交给运维自查。