Files
tamabox/docs/honeypot.md
T
tamakyi db97ac13ed docs: 蜜罐文档补家族伪装与资源开销
honeypot.md:
- 开篇从「一个假 WordPress 后台」改为「按扫描目标伪装成对应后台」,列出 9 个家族
  与各自触发路径;
- §1 补静态资源的处理(回占位内容、不落库、插件 .php 回 404 的理由);
- §3 封禁路径表补一行「Tomcat Basic 认证」的归属,说明 401 也走凭据通道;
- 补资源开销的实际数字,便于运维判断这功能值不值。

cli.md:paths 说明补家族汇总。
2026-10-23 12:38:42 +08:00

253 lines
14 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.
# 蜜罐(虚假后台登录页)
给扫描器一组假的后台登录页(WordPress / phpMyAdmin / Jenkins / Tomcat / Grafana …):它探后台路径时返回对应产品外观的假页面,它提交账号密码时把凭据记下来、并把它临时封掉。
一句话取舍:**这套功能的价值是「拿到对方正在用的字典」,不是「挡住扫描」。** 挡扫描靠 CDN 和 `autoban`;蜜罐只在「有人真的往假后台里填账号密码」时才有意义。
---
## 1. 生效范围
蜜罐挂在 `f.NotFound` 上 —— flamego 只有**路由表完全落空**时才会走到这里,所以:
- `/login`、`/register`、`/_/{domain}`、`/user/**`、`/api/v1/**` 这些真实路由永远不会被接管;
- 命中后短路返回,不查业务表、不渲染业务模板、不调用 `ctx.Next()`;
- 页面是纯静态 HTML,只含 canary 标记和假表单,不含任何真实信息。
匹配规则(`internal/security/honeypot/honeypot.go`):
| 类型 | 例子 |
|---|---|
| 段前缀 | `/admin`、`/wp-admin`、`/.ssh`、`/.env`、`/phpmyadmin`、`/actuator` |
| 任意位置片段 | `/wp-includes/`、`/xmlrpc.php`、`/cgi-bin/`、`/.git/` |
| 文件后缀 | `*.zip`、`*.sql`、`*.bak`、`*.pem`、`*.key`、`*.env` |
| 自定义 | `[security] honeypot_paths`(逗号分隔,与内置清单取并集) |
业务路由前缀(`/login`、`/api`、`/static` 等)即使出现在自定义清单里也会被强制剔除,构成第二道保险。
改完路径后用 `tamabox honeypot paths` 看最终生效的完整清单(会按家族汇总条数)。
### 会伪装成哪几类后台
| 家族 | 典型路径 | 表单字段 / 响应 |
|---|---|---|
| WordPress | `/wp-login.php`、`/wp-admin`、`/wp-json` | `log` / `pwd`,带 `wordpress_test_cookie` |
| phpMyAdmin | `/phpmyadmin`、`/pma`、`/adminer.php` | `pma_username` / `pma_password` |
| Tomcat | `/manager/html`、`/host-manager` | **401 + `WWW-Authenticate`**(Basic,凭据从 `Authorization` 头取) |
| Jenkins | `/jenkins`、`/jnlpJars` | `j_username` / `j_password`,带 `X-Jenkins` 头 |
| Grafana | `/grafana` | `user` / `password`(深色主题) |
| GitLab | `/users/sign_in` | `user[login]` / `user[password]` |
| LuCI(路由器) | `/cgi-bin/luci`、`/luci` | `luci_username` / `luci_password` |
| Joomla | `/administrator` | `username` / `passwd` |
| 通用后台 | `/admin`、`/console`、`/zabbix`、`/jira` … | `username` / `password` |
**为什么要分类**:扫描器和爆破脚本都是「先按路径判断这是什么后台,再用它认识的字段名填表」。所有路径都回 WordPress 页面,等于在 `/phpmyadmin/index.php` 上告诉对方「这里不是 PMA」,对方直接换目标 —— 我们既拿不到凭据,也留不下有效取证。
**静态资源也一起伪装**:`/wp-admin/css/login.min.css`、`/wp-includes/js/*.js`、`/wp-admin/images/*.png` 这类请求回占位 CSS/JS/真 1×1 PNG。这些请求**不落库、不计数** —— 一次页面加载会打 5–10 个资源,逐个 INSERT 等于帮攻击者把写入放大十倍。插件目录下的 `.php` 则回 404(那是 webshell 探测,回登录页反而不像)。
单页渲染约 3.7 µs / 2 次分配、页面 1.5–2.5 KB,资源请求 0 分配(`BenchmarkRenderFamily` / `BenchmarkAssetFor` 守着)。
---
## 2. 配置
```ini
[security]
enable_honeypot = true
honeypot_paths =
honeypot_threshold = 1
honeypot_ban_minutes = 1440
honeypot_retain_days = 30
honeypot_auto_clean = true
honeypot_max_rows = 200000
honeypot_cred_dir = data/honeypot-creds
```
| 键 | 缺省 | 说明 |
|---|---|---|
| `enable_honeypot` | `false` | 总开关。关闭后 `/wp-admin` 等路径恢复原先的 302 → `/` |
| `honeypot_paths` | 空 | 追加诱饵路径,与内置清单取并集 |
| `honeypot_threshold` | `1` | **提交凭据**的封禁阈值(见 §3) |
| `honeypot_ban_minutes` | `1440` | 蜜罐封禁时长;显式 `0` 回退 `ban_duration_minutes` |
| `honeypot_retain_days` | `30` | 元数据与凭据文件的保留天数;`<= 0` 也回退 30 |
| `honeypot_auto_clean` | `true` | 每小时一轮后台清理;关闭后只能手动 `honeypot prune` |
| `honeypot_max_rows` | `200000` | 元数据行数上限(第二道闸);显式 `0` = 不限 |
| `honeypot_cred_dir` | `data/honeypot-creds` | 凭据文件目录,进程工作目录为基准 |
> ⚠️ **配置项一律写成「注释独占一行 + 裸值」**。配置加载器开启了 `IgnoreInlineComment`,行尾 `; 注释` 会被并入值:`honeypot_max_rows = 200000 ; 说明` 解析出来是 `"200000 ; 说明"`,int 解析失败而 `MapTo` 不报错 —— 该项静默回退零值,日志里只会说「配置值不对」。
改完配置**必须重启**(配置只在启动时读一次)。
---
## 3. 两条封禁路径(最容易误解的地方)
| 行为 | 走哪条通道 | 阈值 | 是否立即封 |
|---|---|---|---|
| `GET /wp-login.php` 看一眼假页面 | 常规 `autoban.Record` | `ban_threshold_sensitive`(默认 10 次 / 60 秒) | 否 |
| `GET /wp-admin/css/login.min.css` 等资源 | **不计数、不落库** | — | 否 |
| `POST` 带该家族的凭据字段(`log`/`pwd`、`pma_*`、`j_*`、`user[login]` …) | 蜜罐 `autoban.RecordHoneypot` | `honeypot_threshold`(默认 1) | 是,立即封 |
| `POST` 不带任何凭据字段 | 常规通道 | 同上 | 否 |
**为什么 GET 不立即封**:爆破脚本的标准流程是「先 GET 探表单 → 再 POST 提交」。如果 GET 就吃 403,假登录页只被看了一次,对方根本没机会填表,凭据永远拿不到 —— 而这个功能的全部价值就在那份凭据上。
蜜罐通道**不受 `enable_auto_ban` 约束**:关掉常规自动封禁(它对共享出口误伤多)不会连带让蜜罐变成只记录不封禁的旁观者。
纯探测也不会被刷爆:元数据落库按「每 IP 每分钟一条」采样,凭据则是「有就存」——那才是要保的东西。
### 封的是一把「键」,不是一个人
`autoban` 的表就是内存里的 `map[键]到期时间`,键由 `shared.LimitKey` 生成:
- IPv4 → 原样(`203.0.113.7`)
- IPv6 → 折叠到 `/64`(`2408:8207:1234:5678::/64`),防止在单个 /64 内轮换地址绕过阈值
所以告警邮件里**可能有两个地址**:
| 字段 | 含义 |
|---|---|
| 来源 IP | 精确客户端地址,取证与对账用 |
| 封禁范围 | 实际写进封禁表的键;IPv6 下是 /64 前缀,整段一起封(IPv4 下与来源 IP 相同,不重复显示) |
两个直接后果,排障时先记住:
1. **封禁只在内存里** —— 重启进程即解除,邮件不会撤回;
2. **封的键与请求算出来的键必须一致** —— 换地址族(IPv6 访问 / IPv4 回源)或换 /64 前缀就不会被拦。
每次建封都会打一条 WARN,键、原因、时长、到期时间都在里面:
```bash
journalctl -u tamabox -n 50 | grep "已自动封禁来源"
# ban_key=2408:8207:1234:5678::/64 reason=蜜罐提交凭据 duration=24h0m0s expires_at=...
```
`ban_key` 就是被封的键,与邮件里的「封禁范围」一致。若这里没有记录,说明当时根本没建封(邮件可能是别的通道发的);若有记录但仍能访问,就去比对请求算出来的键。
---
## 4. 凭据存在哪里
**用户名/密码不进数据库**,只追加到按天分文件的 JSONL:
```
data/honeypot-creds/creds-2026-10-21.jsonl
```
```json
{"time":"2026-10-21T16:13:50+08:00","ip":"117.x.x.226","method":"POST",
"path":"/wp-login.php","username":"admin2","password":"test456",
"fields":{"log":"admin2","pwd":"test456"},"raw_body":"log=admin2&pwd=test456",
"user_agent":"curl/8.22.0","banned":true}
```
| 项 | 值 |
|---|---|
| 目录权限 | `0700`(启动时强制 `chmod`) |
| 文件权限 | `0600` |
| 切分 | 按记录时间,一天一个文件 |
| 坏行 | 跳过(进程被杀留下的半条 JSON 不会让整天数据报废) |
| 清理 | 与 `honeypot_retain_days` 同步,只删「整天都过期」的文件 |
**为什么不入库**:明文账号密码一旦入库,就会跟着进备份、主从复制、以及任何有库只读权限的报表/BI 工具,事后无法彻底清除。文件可以单独挂卷、单独设权限、到期直接删。
数据库里只保留「发生过一次带凭据的提交」这个事实(`honeypot_hits.had_fields`),以及 IP / 路径 / UA / 是否封禁这些元数据。
> `ip` 两处存的都是**精确客户端地址**(IPv6 保留完整地址),不是封禁用的 `/64` 聚合键 ——
> 取证要能定位到具体主机。想知道「封了多大范围」,看告警邮件的「封禁范围」或日志里的 `ban_key`。
> ⚠️ 这些是**外部攻击者提交的真实账号密码**,很可能是从别处泄露后复用的。请按敏感数据对待:不要提交进版本库(`/data/` 已在 `.gitignore`)、不要参与备份、不要贴到公开群组。
---
## 5. 部署:反向代理必须配 `trust_proxy`
这是**部署时最容易踩、后果最严重**的一条。
如果站点在 nginx / Cloudflare 之后而 `trust_proxy = false`:
- 源站看到的所有请求都来自代理地址(常见 `127.0.0.1`),**取证数据失去意义**;
- 更糟:一次蜜罐命中封的是 `127.0.0.1`,而所有访客在源站眼里都是这个 IP —— **等于封掉整站**。
正确配置:
```ini
[security]
trust_proxy = true
trusted_proxies = 127.0.0.1
```
> ⚠️ 前提:**源站不能直接被公网访问**(Cloudflare Tunnel / Authenticated Origin Pull / 防火墙只放行回源网段)。`trust_proxy = true` 时程序会信任 `CF-Connecting-IP` 这类权威头,源站若可直连,攻击者能伪造它来绕过封禁、或反手封掉别人。
程序在「命中回环地址且 `trust_proxy` 未开」时会打一条一次性 WARN(24 小时冷却)提醒检查这里。
验证:触发一次蜜罐后看凭据文件里的 `ip` 是不是真实客户端地址。
---
## 6. 升级:清掉历史明文列
早期版本把 `username` / `password` / `fields` / `raw_body` 直接存在 `honeypot_hits` 上。结构体已经删掉这些字段,但 **GORM 的 AutoMigrate 不会删既有列** —— 老库升级上来后,历史明文还在表里。
启动时会打一条 WARN:
```
honeypot_hits 上仍残留旧版明文凭据列,请执行 TamaBox honeypot purge-creds 清除
```
清理(**不可逆**,先备份):
```bash
pg_dump -Fc <dbname> > before-purge-$(date +%F-%H%M).dump
./tamabox honeypot purge-creds # 列出四列,输入 yes 才执行
```
只处理当前库;**历史备份与从库里可能仍有明文**,需要自行处理。
---
## 7. 日常运维
```bash
./tamabox honeypot hits # 最近 24h 的凭据(密码默认脱敏)
./tamabox honeypot hits --ip 1.2.3.4 --show-password
./tamabox honeypot stats --days 7 # 来源排行(IPv6 按 /64 聚合)+ 路径排行 + 凭据排行
./tamabox honeypot paths # 当前生效的诱饵路径
./tamabox honeypot size # 表行数 + 磁盘占用 + 凭据文件体积
./tamabox honeypot prune --days 30 # 手动按保留期清(元数据 + 凭据文件)
```
`hits` / `paths` 不连数据库(DB 挂了也能查)。
---
## 8. 排障
| 现象 | 原因 | 处理 |
|---|---|---|
| **邮件说封了,但还能正常访问** | ① 进程重启过(封禁是内存态)② 后续请求换了地址族 / 换了 /64 前缀 ③ `trust_proxy` 未配,封的是反代地址 | 看日志里 `已自动封禁来源` 的 `ban_key`,与 `honeypot hits` 里的 IP、以及本机 `curl -6 ifconfig.co` 比对;见 §3「封的是一把键」 |
| `/wp-admin` 还是 302 | `enable_honeypot` 未开 / 配置未重启生效 | 看启动日志;`honeypot paths` 确认开关状态 |
| 记录里 `ip` 是 `127.0.0.1` | `trust_proxy` 未配 | 见 §5;**注意此时封禁会封全站** |
| 整站都返回 403 | 蜜罐把反代 IP 封了(上一条的后果) | 重启进程(封禁是内存态)或等 `honeypot_ban_minutes` 到期 |
| GET 假页面不封禁 | 设计如此 | 提交凭据才立即封,见 §3 |
| 记录里 `path` 带前导空白/奇怪字符 | 上游(nginx/CF)改写了 URI | 代码已清洗并打 WARN;根因建议查 `proxy_pass` / `rewrite` |
| `honeypot` 命令不存在 | 二进制旧 / 两个构建入口未同步 | `go build -o tamabox .` 后**重启进程** |
| 改了代码但行为没变 | 只替换了二进制,进程还在跑旧映像 | `kill` 旧进程后按原方式重新拉起 |
| 凭据文件没有生成 | `honeypot_cred_dir` 不可写 | 看启动日志里的「凭据目录不可用」告警 |
| 启动报 `honeypot_hits 上仍残留…` | 老库有历史凭据列 | `honeypot purge-creds`(§6) |
排障时两个容易误判的点:
- **验证二进制里有没有某个中文字符串,不能用 `strings`** —— 它默认只输出可打印 ASCII,UTF-8 中文整段被丢弃,`strings x | grep 中文` 恒为 0。用 `grep -ac "中文字符串" x`。
- **替换二进制 ≠ 生效**,进程必须重启;前台跑 `./tamabox web` 若立刻返回,通常是端口被旧进程占着。
- **邮件里的「来源 IP」不是封禁键**:IPv6 下前者是精确地址、后者是 /64 前缀,拿前者去查后者永远对不上。判「封没封」只看 `ban_key`。
---
## 9. 设计取舍(已知限制)
- **GET 探测不封**:见 §3。代价是扫描器可以反复拿假页面,收益是能拿到凭据。
- **假页面带 canary**:只放在 HTML 注释里(`<!-- build: tb-hp-... -->`),用于确认「这确实是蜜罐返回的页面」;`<meta name="generator">` 伪装成 `WordPress 6.4.3`,不主动暴露技术栈。
- **只挡 NotFound**:如果将来给 `/admin` 之类路径注册了真实路由,蜜罐会自动让位 —— 这是刻意设计(业务优先)。
- **告警邮件仍带明文用户名/密码**:这正是它的价值(看到对方在用什么字典),但也意味着邮件是敏感物,收件人只应是你自己。
- **不主动反制**:不回打、不上报第三方、不做删除/篡改,只有「记录 + 临时封禁」。