Files
tamabox/docs/honeypot.md
T
tamakyi 90a284a3dd docs: 写清「封的是一把键」与取证地址的区别
honeypot.md:
- §3 新增小节:LimitKey 折叠规则(IPv4 原样 / IPv6 折 /64)、邮件里两个地址
  各自含义、封禁只在内存里、建封 WARN 的字段与 grep 方法;
- §4 注明取证存的是精确地址,不是 /64 聚合键;
- §8 排障首行补「邮件说封了但还能访问」的三种原因与判定路径,
  并提醒「邮件里的来源 IP 不是封禁键,拿它对 ban_key 永远对不上」。
cli.md:stats 说明改为「来源排行(IPv6 按 /64 聚合)」,补 ip 语义。
2026-10-23 10:53:12 +08:00

12 KiB
Raw Blame History

蜜罐(虚假后台登录页)

给扫描器一个假的 WordPress 登录页:它探后台路径时返回 200 假页面,它提交账号密码时把凭据记下来、并把它临时封掉。

一句话取舍:这套功能的价值是「拿到对方正在用的字典」,不是「挡住扫描」。 挡扫描靠 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 看最终生效的完整清单。


2. 配置

[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 秒) 否
POST 带 log / pwd 提交 蜜罐 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,键、原因、时长、到期时间都在里面:

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
{"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 —— 等于封掉整站。

正确配置:

[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 清除

清理(不可逆,先备份):

pg_dump -Fc <dbname> > before-purge-$(date +%F-%H%M).dump
./tamabox honeypot purge-creds        # 列出四列,输入 yes 才执行

只处理当前库;历史备份与从库里可能仍有明文,需要自行处理。


7. 日常运维

./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 之类路径注册了真实路由,蜜罐会自动让位 —— 这是刻意设计(业务优先)。
  • 告警邮件仍带明文用户名/密码:这正是它的价值(看到对方在用什么字典),但也意味着邮件是敏感物,收件人只应是你自己。
  • 不主动反制:不回打、不上报第三方、不做删除/篡改,只有「记录 + 临时封禁」。