Files
tamabox/.workbuddy/memory/2026-10-21.md
T
tamakyi 7d52155411 fix(honeypot): 蜜罐封禁与 enable_auto_ban 解耦 + 泄露扫描后缀匹配
- autoban: 新增 RecordHoneypot 独立通道(不受 enabled 约束)+ 独立阈值/时长;
  IsBanned 不再因 enable_auto_ban=false 而忽略已有封禁
- honeypot: 新增 honeypotSuffixes 后缀匹配,覆盖 /backup.zip、/db.sql、/id_rsa.pem
- 新增配置 honeypot_threshold / honeypot_ban_minutes
- 补测试:autoban/honeypot_test.go、route 侧禁用自动封禁仍封禁
2026-10-21 12:36:33 +08:00

107 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-10-21
## 扫描者处置方案设计(未实施,仅设计文档)
- 触发:线上频繁收到「疑似批量扫描已自动封禁」告警,命中路径以 wp 探测 + `/.ssh/**` 为主,来源多 Azure 云 IP 池。
- **实测纠正一处错误认知**:`internal/route/route.go:432` 的注释称「flamego 的 NotFound 会先写 404 状态再 Redirect」——
**错的**。flamego v1.9.4 的 `NotFound` 只执行 handler,不写状态码;`ctx.Redirect("/")` 实测输出 **302 → "/"`。
最小复现验证:`/wp-admin/includes/...` 与 `/.ssh/id_rsa` 均得 302 + Location:/。
后果:扫描器跟随重定向 → 每个探测路径都触发一次首页渲染(1C1G 纯浪费),且被判为「站点有价值」。
同时告警邮件文案「产生大量 404」与实际 302 不符。
- 方案分五层(L0 CDN 路径/UA 规则 → L1 真 404 分流 → L2 蜜罐假文件/假凭据 → L3 tarpit + 递进封禁 → L4 上游 abuse 举报):
- 核心判断:**对云厂商僵尸网络做 IP 封禁无效**,有效的是按**路径**在 CDN 拦 + 蜜罐取证 + 上游举报。
- 明确排除反向攻击/DoS/回打(法律风险 + Azure 共享出口伤及无辜 + 招致 CDN 封站)。
- 蜜罐:命中 `/.ssh/id_rsa`、`/.env`、`/wp-login.php` 等诱饵路径 → 短路返回假内容(含 canary 标记)+ 永久封禁 + 留证。
- 封禁需落库(当前内存 map,重启清零)+ 时长递进 15m→1h→6h→24h→7d→永久 + 多维(IP / IPv6 /64 / UA 家族)。
- tarpit 必须带全局并发上限 + 单连接超时,否则 1C1G 自身变 DoS 入口。
- 设计文档:`outputs/scanner-defense-design-2026-10-21.md`(含分级变更清单、文件级改动点、验收指标)。
- 未动任何生产代码,等用户决定实施范围。
## 蜜罐(虚假后台登录界面 + 破解凭据记录)已实施
用户明确需求原文:「能不能把这些页面指向一个简单的虚假后台登录界面,然后记录这些破解的用户名密码等信息」。
### 落点决策(关键)
- **蜜罐不改写成新中间件,而是改写 `internal/route/route.go` 的 `f.NotFound`**:
- NotFound 只在「路由表完全落空」时触发 → 与 `/login` `/register` `/_/{domain}` `/user/**` `/api/v1/**` 天然零交集;
- 位于全局 `f.Use` 链之后 → `requestBodyLimitMiddleware` 已给 body 加 12 MiB 上限;
- 组级中间件(含 CSRF)只挂具体路由,不作用于 NotFound → POST 表单可安全解析;
- flamego `context.run()` 在 `ResponseWriter().Written()` 为真时 return → 写完响应不调 `ctx.Next()` 即安全短路。
- **双重计数坑**:autoban 中间件对敏感路径已 `Record` 一次,蜜罐再记一次会让阈值实际减半。
解决:`autobanMiddleware(b, hp)` 传入蜜罐匹配器,蜜罐命中的路径由蜜罐自己计数。
### 新增文件
- `internal/security/honeypot/honeypot.go` — 匹配器(段前缀 + 片段子串 + 双重保险 `forbiddenPrefixs` 排除业务路由)、
凭据提取(表单 / JSON / BasicAuth / 查询串)、`CanaryToken = "tb-hp-7f3a1c9e"`。
- `internal/security/honeypot/page.go` — 仿 WordPress 假登录页(经典字段 `log`/`pwd`/`redirect_to`),内联 CSS 不依赖站点静态资源。
- `internal/db/honeypot.go` — `HoneypotHit`(**不嵌 dbutil.Model**,审计证据只追加)+ `HoneypotStore`。
- `internal/route/honeypot.go` — 处理器:GET 假登录页 / POST 假失败响应 / 落库 / 复用 autoban 封禁 / 每 IP 告警一次。
- `internal/cmd/honeypot.go` — `TamaBox honeypot hits|stats|prune`,密码默认脱敏(`--show-password` 显示明文)。
- `templates/mail/honeypot-alert.html` — 告警邮件模板(`mail/` 已在 embed 清单,无需改 embed.go)。
### 改动文件
- `internal/route/route.go`(CRLF):NotFound 分流 + 蜜罐实例装配 + `hph.sendAlert`。
- `internal/route/autoban.go`(LF):`autobanMiddleware(b, hp)`,蜜罐路径跳过自身 Record。
- `internal/db/db.go`(LF):`AllTables` 加 `&HoneypotHit{}`,`Init()` 加 `Honeypots = NewHoneypotStore(db)`。
- `internal/conf/static.go`(CRLF):`enable_honeypot` / `honeypot_paths` / `honeypot_retain_days`。
- `conf/app.sample.ini`(CRLF):对应说明段。
- `internal/mail/mail.go`(LF):`SendHoneypotAlertMail` + `HoneypotAlert`。
- `cmd/tamabox.go` + 根 `tamabox.go`:注册 `cmd.Honeypot`(两处必须同步)。
### 踩坑
- `normalize()` 里 `for len(p)>1 { TrimSuffix(p,"/") }` **无限循环**(路径不含尾斜杠时字符串不变)→ 改成 `HasSuffix` 判断。
- `url.ParseQuery` 遇非法百分号编码返回 error 但仍给出部分字段 → 蜜罐要「尽可能多拿凭据」,忽略 error 继续用部分结果。
- JSON 凭据提取:手写解析器又脏又易错 → 改用 `jsonPairRe` 正则扫描扁平字段。
- `templates/mail/` 已在 embed 清单内 → 新增邮件模板**不需要**改 `templates/embed.go`(顶层文件才需要)。
### 验证
- `go build ./...` 通过;`go test ./...` 全绿(含 DB 测试,postgres 门控)。
- 新增测试:`internal/security/honeypot/honeypot_test.go`(匹配/业务路由零交集/编码绕过/凭据提取)、
`internal/route/honeypot_test.go`(假页含 canary、业务路由不被劫持、关闭时回落 302、POST 返回失败页)、
`internal/db/honeypot_test.go`(写入/倒序/三张聚合表/保留期清理)。
## 测试站黑盒验证 + P0 缺陷修复(蜜罐封禁被 enable_auto_ban 连坐)
测试站:`https://box-dev-api.337845818.best`(Cloudflare Tunnel + CF 边缘,`cf-cache-status: DYNAMIC`、无 `Age` → 不缓存,请求确实回源)。
生产站 `https://box.tama.guru` 强制 301 → www,本机不可达,无法做对照。
### 黑盒验证结果(全部通过)
- 诱饵路径全部 200 + `text/html`:`/wp-login.php` 2529B、`/wp-admin/` 2517B、`/.ssh/id_rsa` 2525B、`/.env` 2497B、`/administrator` 2533B。
- 响应体含 canary `tb-hp-7f3a1c9e`(2 处)、`<title>Log In ‹ WordPress</title>`、`name="log"`/`name="pwd"`/`name="redirect_to"`。
- 响应头:`Cache-Control: no-store, no-cache, must-revalidate`、`x-robots-tag: noindex, nofollow`、`x-frame-options: DENY`。
- POST 凭据提交 → 200 + `id="login_error">ERROR: Invalid username or password.`。
- 路径变体全命中:`/WP-ADMIN`(大小写)、`/wp-admin%2f..%2fetc`(编码绕过)、`//wp-admin//x.php`(双斜杠)、`/.git/config`、`/.aws/credentials`、`/phpmyadmin/`、`/actuator/env`、`/xmlrpc.php`、`/vendor/phpunit/x.php`、`/console`、`/cgi-bin/test.cgi`、`/wp-content/plugins/x.php`、`/.env.bak`、`/wp-json/wp/v2/users`。
- 凭据来源全覆盖:表单 POST、JSON body、BasicAuth、查询串、POST 到 `/.ssh/id_rsa`。
- **业务路由零交集(红线)**:`/` 34192B、`/login` 23420B、`/register` 25194B、`/_/admin6` 39358B、`/user` 302、`/api/v1/questions` 302、`/robots.txt` 145B;非诱饵随机路径 `/nonexistent-random-xyz` 仍 302→`/`。
### P0 缺陷:蜜罐命中 70+ 次但站点零封禁
- 根因:`autoban.Record()` 第一行 `if !b.enabled { return }`,`b.enabled` 来自 `enable_auto_ban`。
运维为避免共享出口误伤而关掉 `enable_auto_ban` → **蜜罐被另一个开关连坐**,只落库不封禁。
- 对照组证明非蜜罐专属:`/index.php`(autoban 敏感、蜜罐不匹配)连打 40 次 + 30 并发同样零封禁。
- 排除项:CF 缓存(DYNAMIC/无 Age)、flamego NotFound 链(源码确认全局 Use 链生效)、蜜罐匹配器、阈值配置。
**修复(已实施,测试全绿)**:
- `autoban.Options` 新增 `HoneypotEnabled` / `HoneypotThreshold`(默认 1)/ `HoneypotDuration`(默认复用常规)。
- 新增 `Banner.RecordHoneypot(ip, path) bool`:独立计数、独立阈值,**不受 `enabled` 约束**;键与 `Record` 一致(同样 `shared.LimitKey` 归一化),否则 `IsBanned` 判不出来。
- `IsBanned()` 移除 `!b.enabled` 早退 —— `enabled` 只控制「是否主动创建封禁」,不控制「是否尊重已有封禁」。
- `internal/route/honeypot.go`:`banned = h.ab.RecordHoneypot(ip, path)`。
- `internal/route/route.go`(CRLF)装配三个蜜罐字段;`conf/static.go` + `conf/app.sample.ini`(CRLF)新增 `honeypot_threshold` / `honeypot_ban_minutes`。
- 告警:仍走既有 `sendAlert`(「已封禁」)+ 蜜罐自己的告警邮件(「捕获了什么凭据」),互补。
### 后缀匹配补缺
- `/backup.zip`、`/db.sql`、`/id_rsa.pem` 这类泄露扫描文件名主体随机,**只有后缀稳定**。
- 原 `honeypotFragments` 用 `"/.zip"` 子串匹配 → `.zip` 前是 `k` 不是 `/`,全漏网。
- 新增 `honeypotSuffixes`(`.zip .tar .tar.gz .tgz .gz .rar .7z .bz2 .sql .sql.gz .dump .bak .old .orig .save .swp .env .env.bak .env.local .pem .key .p12 .pfx .jks .kdbx .ovpn`),按 `path.Base()` 结尾匹配。
### 踩坑
- ⚠️ **测试里 `Duration: 60` = 60 纳秒**,封禁瞬间过期 → 表现为「时序巧合的 200/200/403」,测试假失败。一律写 `60 * time.Minute`。
- ⚠️ 路由测试只注册路由 + NotFound 会被绕过:执行 `IsBanned` → 403 的是 **`autobanMiddleware`**,测试服务器必须 `f.Use(autobanMiddleware(ab, hp))`。
- ⚠️ `internal/route/route.go`、`internal/conf/static.go`、`conf/app.sample.ini` **实际是全 CRLF**;`grep | cat -A` 不显示 `^M`,极易误判为 LF。Python 脚本按 `\n` 锚点替换会静默失败 → 用 `\r\n` 锚点 + 写后统计 `b'\r\n'` 复核。
- 假页纯静态 HTML(内联 CSS,不落盘)、告警走 SMTP → **取证数据只在 `honeypot_hits` 一张表**,服务器无文件副本;autoban 封禁表是内存态(重启清零)。
- 测试阶段产生的噪声凭据(`testadmin/S3cr3tP@ss` 等)可用 `TamaBox honeypot prune` 清理。
### 遗留(需用户侧)
- 本轮修复需**重新部署**才在测试站生效;部署后 `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`。