fix(honeypot): 取证记精确地址、封禁用 /64 键,并补建封与拦截日志
线上实测:IPv6 访问蜜罐后收到「你的 IP 已被封禁」,邮件里却是 `2408:…::/64` —— 既不像自己的地址,也说不清封了多大范围;同时用户自己 仍能正常访问,看日志也无从判断到底有没有封。 根因是「取证地址」与「封禁键」混用同一个变量(都取 LimitKey 的折叠值): - 取证丢掉精确 IPv6 后 64 位,事后无法定位到具体主机、没法和其他日志对账; - 邮件只给聚合键,运维第一反应是「不是我」,排查方向直接被带偏。 改动: - Serve 拆 clientIP(ResolveClientIP,精确)与 banKey(LimitKey,/64): DB / 凭据文件 / 告警邮件用前者,封禁 / 探测采样 / 告警去重用后者; - autoban.Options 新增 OnBan 回调(保持纯标准库、不引入 logrus), route.newBanner 注入后每次建封打 WARN:ban_key / reason / duration / expires_at; 中间件 403 分支加 Debug 日志(被封者会持续请求,Info 会被刷屏); - 告警邮件新增「封禁范围」行,仅 ip != ban_key 时渲染(IPv4 不重复显示), 顺带把过时的「已入库」改为「写入取证文件」——凭据早就外置了; - honeypot stats 的来源排行改按 LimitKey 合并,否则 IPv6 轮换地址会把 排行碎成几十条各 1 次的记录;合并口径与封禁完全一致。 测试(每项都做了变异验证:改坏→确认变红→恢复): - TestHoneypotRecordsExactIPv6ButBansByPrefix:精确地址进 DB 与凭据文件、 /64 才是封禁键、同 /64 换地址仍 403; - TestOnBanCallback:建封回调带键/时长/原因,封禁期内重复命中不重复回调; - TestHoneypotAlertTemplateRenders:IPv6 渲染封禁范围、IPv4 不重复; - TestMergeSourcesByKey:同 /64 合并、IPv4 原样、limit 与空输入。
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/flamego/flamego"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/security/autoban"
|
||||
"github.com/tamakyi/TamaBox/internal/security/honeypot"
|
||||
@@ -25,13 +26,19 @@ func autobanMiddleware(b *autoban.Banner, hp *honeypot.Honeypot) flamego.Handler
|
||||
if ip == "" {
|
||||
return
|
||||
}
|
||||
path := ctx.Request().URL.Path
|
||||
if b.IsBanned(ip) {
|
||||
// Debug 级别:被封住的扫描器会持续请求,Info 会让日志彻底淹没。
|
||||
// 需要现场确认「封禁真的在拦」时把日志级别调到 debug 即可。
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"ban_key": ip,
|
||||
"path": path,
|
||||
}).Debug("拒绝已封禁来源")
|
||||
ctx.ResponseWriter().WriteHeader(http.StatusForbidden)
|
||||
_, _ = ctx.ResponseWriter().Write([]byte("403 Forbidden"))
|
||||
return
|
||||
}
|
||||
|
||||
path := ctx.Request().URL.Path
|
||||
kind := autoban.KindNone
|
||||
// 蜜罐命中的路径由蜜罐处理器自己计数(它需要同时知道「是否触发了封禁」
|
||||
// 以便落库)。若这里再记一次,同一次请求会被计两遍,阈值实际减半。
|
||||
|
||||
+57
-30
@@ -131,7 +131,16 @@ func newHoneypotHandler(hp *honeypot.Honeypot, ab *autoban.Banner, store db.Hone
|
||||
// Serve 处理一次蜜罐命中。
|
||||
func (h *honeypotHandler) Serve(ctx flamego.Context) {
|
||||
req := ctx.Request().Request
|
||||
ip := shared.LimitKey(shared.ResolveClientIP(req))
|
||||
// 两个不同的东西,必须分开用:
|
||||
//
|
||||
// clientIP —— 精确客户端地址(IPv6 保留完整地址),取证与告警展示用;
|
||||
// banKey —— 聚合键(shared.LimitKey,IPv6 折成 /64),封禁与采样去重用。
|
||||
//
|
||||
// 曾经两者混用(都取 banKey),后果是取证只留下 /64 前缀:运维收到
|
||||
// 「你的 IP 已被封禁」时发现邮件里那个地址根本不是自己的,排查方向直接被带偏;
|
||||
// 而且精确地址一丢,拿去查其他日志或做 abuse 投诉就再也对不上了。
|
||||
clientIP := shared.ResolveClientIP(req)
|
||||
banKey := shared.LimitKey(clientIP)
|
||||
// 反代/网关可能把 URI 带上首尾空白:实测 Cloudflare → nginx → 源站 链路上
|
||||
// 出现过 ` /wp-login.php`。Match 内部会 normalize(含 TrimSpace),所以能命中,
|
||||
// 但落库与假表单的 action 用的是原始值 —— 会留下脏取证数据,
|
||||
@@ -141,9 +150,10 @@ func (h *honeypotHandler) Serve(ctx flamego.Context) {
|
||||
// 这类字符肉眼不可见,裸打印会看不出哪里不同(曾经为此排查了一轮)。
|
||||
if raw := req.URL.Path; raw != path {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"ip": ip,
|
||||
"raw": fmt.Sprintf("%q", raw),
|
||||
"used": fmt.Sprintf("%q", path),
|
||||
"ip": clientIP,
|
||||
"ban_key": banKey,
|
||||
"raw": fmt.Sprintf("%q", raw),
|
||||
"used": fmt.Sprintf("%q", path),
|
||||
}).Warn("蜜罐路径含上游注入的空白/零宽字符,已清洗后使用")
|
||||
}
|
||||
|
||||
@@ -151,7 +161,7 @@ func (h *honeypotHandler) Serve(ctx flamego.Context) {
|
||||
// 典型信号。此时所有访客在源站眼里都是同一个 IP:
|
||||
// 取证数据失去意义,且一次封禁会连带封掉整站(包括普通访客)。
|
||||
// 只告警不改变行为 —— 本地直连调试时回环 IP 是正常的。
|
||||
if ip != "" && !conf.Security.TrustProxy && isLoopbackIP(ip) {
|
||||
if clientIP != "" && !conf.Security.TrustProxy && isLoopbackIP(clientIP) {
|
||||
if h.dedup.first("warn:loopback", 24*time.Hour) {
|
||||
logrus.Warn("蜜罐记录的来源 IP 是回环地址(127.0.0.1 / ::1):站点很可能在反向代理后面," +
|
||||
"[security] trust_proxy 未开启。此时所有访客共享同一个 IP —— 取证失去意义," +
|
||||
@@ -173,11 +183,11 @@ func (h *honeypotHandler) Serve(ctx flamego.Context) {
|
||||
//
|
||||
// 纯探测不封也不会被刷爆:落库有「每 IP 每分钟一条」的采样(见 record)。
|
||||
banned := false
|
||||
if h.ab != nil && ip != "" {
|
||||
if h.ab != nil && banKey != "" {
|
||||
if creds.HadFields {
|
||||
banned = h.ab.RecordHoneypot(ip, path)
|
||||
banned = h.ab.RecordHoneypot(banKey, path)
|
||||
} else {
|
||||
h.ab.Record(ip, path, autoban.KindSensitive)
|
||||
h.ab.Record(banKey, path, autoban.KindSensitive)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +202,7 @@ func (h *honeypotHandler) Serve(ctx flamego.Context) {
|
||||
}
|
||||
|
||||
// 落库留证。落库失败只记日志,绝不影响已写出的响应。
|
||||
h.record(ip, req, path, creds, banned)
|
||||
h.record(clientIP, banKey, req, path, creds, banned)
|
||||
}
|
||||
|
||||
// cleanPath 去掉路径首尾的空白与零宽字符,并保证前导斜杠。
|
||||
@@ -266,12 +276,14 @@ func (h *honeypotHandler) writeLogin(ctx flamego.Context, path, notice string) {
|
||||
|
||||
// record 追加一条命中记录(列宽对齐 + 探测采样都在这里收口)。
|
||||
//
|
||||
// clientIP 是精确客户端地址(写进取证数据),banKey 是聚合键(只用于采样去重)。
|
||||
// 采样理由:honeypot_hits 的写入完全由外部流量驱动,一个诱饵 404 就 INSERT
|
||||
// 一行,等于把「零成本探测」放大成数据库写压力;而 20 万行的数量闸每小时
|
||||
// 才跑一轮,挡不住短时洪泛。提交了凭据的请求才是高价值证据(必须一条不漏),
|
||||
// 纯路径探测按 IP + 窗口采样即可 —— 路径分布仍然保留。
|
||||
func (h *honeypotHandler) record(ip string, req *http.Request, path string, creds honeypot.Credentials, banned bool) {
|
||||
if !creds.HadFields && !h.dedup.first("probe:"+ip, probeSampleWindow) {
|
||||
// 纯路径探测按来源 + 窗口采样即可 —— 路径分布仍然保留。
|
||||
func (h *honeypotHandler) record(clientIP, banKey string, req *http.Request, path string, creds honeypot.Credentials, banned bool) {
|
||||
// 采样按聚合键:同一 /64 内轮换地址的探测属于同一个来源,不必各存一条。
|
||||
if !creds.HadFields && !h.dedup.first("probe:"+banKey, probeSampleWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -283,10 +295,10 @@ func (h *honeypotHandler) record(ip string, req *http.Request, path string, cred
|
||||
// 凭据先落文件:这是不可再生的证据,优先级高于元数据入库。
|
||||
// 写失败只告警 —— 响应已经发出去了,取证失败不能反过来影响请求。
|
||||
if creds.HadFields {
|
||||
h.saveCredential(ip, req, path, query, creds, banned)
|
||||
h.saveCredential(clientIP, req, path, query, creds, banned)
|
||||
}
|
||||
|
||||
provider, proxied := ipinfo.Lookup(ip)
|
||||
provider, proxied := ipinfo.Lookup(clientIP)
|
||||
|
||||
// 元数据入库。这里刻意只有「发生过一次带凭据的提交」这个事实(HadFields),
|
||||
// 不含任何凭据内容 —— 见 db.HoneypotHit.HadFields 的注释。
|
||||
@@ -294,7 +306,7 @@ func (h *honeypotHandler) record(ip string, req *http.Request, path string, cred
|
||||
// 以 `value too long for type character varying(N)` 整条插入失败 ——
|
||||
// 取证记录静默丢失,而攻击者只要发一个超长 URL 就能稳定触发。
|
||||
hit := &db.HoneypotHit{
|
||||
IP: truncate(ip, 64),
|
||||
IP: truncate(clientIP, 64),
|
||||
Method: truncate(req.Method, 16),
|
||||
Path: truncate(path, 500),
|
||||
Query: truncate(query, 1000),
|
||||
@@ -311,13 +323,14 @@ func (h *honeypotHandler) record(ip string, req *http.Request, path string, cred
|
||||
// 只有真的提交了凭据才告警:纯路径探测(GET)没有取证价值,
|
||||
// 而爆破脚本往往在封禁前反复 POST,只告警一次即可。
|
||||
if creds.HadFields {
|
||||
h.alert(ip, req, path, creds, provider, proxied, banned)
|
||||
h.alert(clientIP, banKey, req, path, creds, provider, proxied, banned)
|
||||
}
|
||||
|
||||
// 有凭据的提交才是高价值证据,日志里点名;纯探测只记路径。
|
||||
if creds.HadFields {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"ip": ip,
|
||||
"ip": clientIP,
|
||||
"ban_key": banKey,
|
||||
"path": path,
|
||||
"username": creds.Username,
|
||||
"banned": banned,
|
||||
@@ -331,7 +344,7 @@ func (h *honeypotHandler) record(ip string, req *http.Request, path string, cred
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := h.store.Create(ctx, hit); err != nil {
|
||||
logrus.WithError(err).WithField("ip", ip).WithField("path", path).
|
||||
logrus.WithError(err).WithField("ip", clientIP).WithField("path", path).
|
||||
Warn("honeypot: failed to persist hit")
|
||||
}
|
||||
}
|
||||
@@ -345,15 +358,17 @@ func (h *honeypotHandler) record(ip string, req *http.Request, path string, cred
|
||||
//
|
||||
// 文件写入失败只告警不重试:蜜罐是尽力而为的取证,不能因为磁盘满
|
||||
// 反过来影响已经发出的响应;而且重试会放大攻击者的写入能力。
|
||||
func (h *honeypotHandler) saveCredential(ip string, req *http.Request, path, query string, creds honeypot.Credentials, banned bool) {
|
||||
// clientIP 必须是精确地址(不是 shared.LimitKey 折叠后的 /64 键):
|
||||
// 这份文件是取证物,IPv6 只留前缀等于把「哪台主机」永久丢掉了。
|
||||
func (h *honeypotHandler) saveCredential(clientIP string, req *http.Request, path, query string, creds honeypot.Credentials, banned bool) {
|
||||
if h.cstore == nil {
|
||||
logrus.WithField("ip", ip).
|
||||
logrus.WithField("ip", clientIP).
|
||||
Warn("蜜罐捕获到凭据,但凭据目录不可用,本条凭据未被保存")
|
||||
return
|
||||
}
|
||||
rec := honeypot.CredRecord{
|
||||
Time: time.Now(),
|
||||
IP: truncate(ip, 64),
|
||||
IP: truncate(clientIP, 64),
|
||||
Method: truncate(req.Method, 16),
|
||||
Path: truncate(path, 500),
|
||||
Query: truncate(query, 1000),
|
||||
@@ -365,18 +380,26 @@ func (h *honeypotHandler) saveCredential(ip string, req *http.Request, path, que
|
||||
Banned: banned,
|
||||
}
|
||||
if err := h.cstore.Append(rec); err != nil {
|
||||
logrus.WithError(err).WithField("ip", ip).Warn("蜜罐凭据写入文件失败")
|
||||
logrus.WithError(err).WithField("ip", clientIP).Warn("蜜罐凭据写入文件失败")
|
||||
}
|
||||
}
|
||||
|
||||
// alert 发送一次告警邮件(每个 IP 只发一次)。
|
||||
func (h *honeypotHandler) alert(ip string, req *http.Request, path string, creds honeypot.Credentials, provider string, proxied, banned bool) {
|
||||
if h.sendAlert == nil || ip == "" {
|
||||
// alert 发送一次告警邮件(同一来源在冷却期内只发一封)。
|
||||
//
|
||||
// clientIP 进正文(人要看的是「谁来了」,必须是精确地址);
|
||||
// banKey 用作去重键,同时也让邮件能说清「实际被封的是哪个键」——
|
||||
// IPv6 下两者不同(键是 /64 前缀),邮件只给其中一个都会让人误判。
|
||||
func (h *honeypotHandler) alert(clientIP, banKey string, req *http.Request, path string, creds honeypot.Credentials, provider string, proxied, banned bool) {
|
||||
if h.sendAlert == nil || clientIP == "" {
|
||||
return
|
||||
}
|
||||
// 同一 IP 在冷却期内只发一封:爆破脚本会在封禁前反复 POST,
|
||||
// 不去重会把邮箱打爆(且每个源 IP 都是一封)。
|
||||
if !h.dedup.first("alert:"+ip, alertDedupTTL) {
|
||||
dedupKey := banKey
|
||||
if dedupKey == "" {
|
||||
dedupKey = clientIP
|
||||
}
|
||||
// 同一来源在冷却期内只发一封:爆破脚本会在封禁前反复 POST,
|
||||
// 不去重会把邮箱打爆。按聚合键去重 —— 同一 /64 内换地址仍是同一个人。
|
||||
if !h.dedup.first("alert:"+dedupKey, alertDedupTTL) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -391,7 +414,8 @@ func (h *honeypotHandler) alert(ip string, req *http.Request, path string, creds
|
||||
}
|
||||
go func() {
|
||||
if err := h.sendAlert(mail.HoneypotAlert{
|
||||
IP: ip,
|
||||
IP: clientIP,
|
||||
BanKey: banKey,
|
||||
Provider: provider,
|
||||
Proxied: proxied,
|
||||
Path: path,
|
||||
@@ -400,7 +424,10 @@ func (h *honeypotHandler) alert(ip string, req *http.Request, path string, creds
|
||||
Banned: banned,
|
||||
DurationMin: durationMin,
|
||||
}); err != nil {
|
||||
logrus.WithError(err).WithField("ip", ip).Warn("honeypot: failed to send alert mail")
|
||||
logrus.WithError(err).WithFields(logrus.Fields{
|
||||
"ip": clientIP,
|
||||
"ban_key": banKey,
|
||||
}).Warn("honeypot: failed to send alert mail")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
|
||||
"github.com/flamego/flamego"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/conf"
|
||||
"github.com/tamakyi/TamaBox/internal/db"
|
||||
"github.com/tamakyi/TamaBox/internal/security/autoban"
|
||||
"github.com/tamakyi/TamaBox/internal/security/honeypot"
|
||||
)
|
||||
@@ -360,3 +363,147 @@ func TestHoneypotBanBlocksBusinessRoutes(t *testing.T) {
|
||||
t.Errorf("status = %d, want 403(被封禁的 IP 不应还能访问业务路由)", resp2.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHoneypotRecordsExactIPv6ButBansByPrefix 验证「取证地址」与「封禁键」是两个东西。
|
||||
//
|
||||
// 线上踩过:用 IPv6 访问蜜罐后收到「你的 IP 已被封禁」,邮件里却是
|
||||
// 2408:…::/64 —— 既不像自己的地址,也说不清到底封了多大范围,
|
||||
// 排查方向直接被带偏。现在取证与邮件给精确地址,封禁仍用 /64 聚合键
|
||||
// (抗「单个 /64 内轮换地址」绕过)。两个断言都必须成立,否则说明又混用了。
|
||||
func TestHoneypotRecordsExactIPv6ButBansByPrefix(t *testing.T) {
|
||||
oldTrust := conf.Security.TrustProxy
|
||||
conf.Security.TrustProxy = true
|
||||
t.Cleanup(func() { conf.Security.TrustProxy = oldTrust })
|
||||
|
||||
cs, err := honeypot.NewFileCredStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 落库路径也要断言:文件与 DB 用的是同一个 clientIP,但任一处写错
|
||||
// 都会让取证「只剩前缀」,而单测只有把它抓在手里才知道。
|
||||
store := &captureStore{}
|
||||
|
||||
f := flamego.New()
|
||||
hp := honeypot.New(honeypot.Options{Enabled: true})
|
||||
ab := autoban.New(autoban.Options{
|
||||
Enabled: true,
|
||||
HoneypotEnabled: true,
|
||||
ThresholdSensitive: 10,
|
||||
Window: time.Minute,
|
||||
Duration: time.Hour,
|
||||
HoneypotThreshold: 1,
|
||||
HoneypotDuration: 24 * time.Hour,
|
||||
})
|
||||
hph := newHoneypotHandler(hp, ab, store, cs)
|
||||
f.Use(autobanMiddleware(ab, hp))
|
||||
f.NotFound(func(ctx flamego.Context) {
|
||||
if hp.Match(ctx.Request().URL.Path) {
|
||||
hph.Serve(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/")
|
||||
})
|
||||
ts := httptest.NewServer(f)
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
const clientIP = "2408:8207:1234:5678::abcd"
|
||||
const banKey = "2408:8207:1234:5678::/64"
|
||||
|
||||
post, err := http.NewRequest(http.MethodPost, ts.URL+"/wp-login.php",
|
||||
strings.NewReader("log=admin&pwd=hunter2"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
post.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
post.Header.Set("CF-Connecting-IP", clientIP)
|
||||
resp, err := http.DefaultClient.Do(post)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
recs, err := cs.ListSince(time.Now().Add(-time.Hour), "", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(recs) != 1 {
|
||||
t.Fatalf("凭据记录数 = %d, want 1", len(recs))
|
||||
}
|
||||
if recs[0].IP != clientIP {
|
||||
t.Errorf("取证 IP = %q, want 精确地址 %q(不能写 /64 聚合键)", recs[0].IP, clientIP)
|
||||
}
|
||||
|
||||
if len(store.hits) != 1 {
|
||||
t.Fatalf("落库记录数 = %d, want 1", len(store.hits))
|
||||
}
|
||||
if store.hits[0].IP != clientIP {
|
||||
t.Errorf("落库 IP = %q, want 精确地址 %q(元数据表同样是取证物)", store.hits[0].IP, clientIP)
|
||||
}
|
||||
|
||||
if !ab.IsBanned(banKey) {
|
||||
t.Errorf("封禁键 %q 未生效:IPv6 必须按 /64 聚合", banKey)
|
||||
}
|
||||
if ab.IsBanned(clientIP) {
|
||||
t.Errorf("精确地址 %q 不应单独成为封禁键:写入与判定必须是同一口径", clientIP)
|
||||
}
|
||||
|
||||
// 同 /64 内换个地址仍应被拦 —— 这才是按前缀聚合的实际价值。
|
||||
get, err := http.NewRequest(http.MethodGet, ts.URL+"/anything", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
get.Header.Set("CF-Connecting-IP", "2408:8207:1234:5678::9999")
|
||||
resp2, err := http.DefaultClient.Do(get)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
if resp2.StatusCode != http.StatusForbidden {
|
||||
t.Errorf("同 /64 内换地址 status = %d, want 403", resp2.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureStore 是 db.HoneypotStore 的极简实现:只把 Create 的记录留下,
|
||||
// 其余方法返回零值。真库测试有 DB 门控、不适合放进这组结构性用例,
|
||||
// 而「落库的 IP 是不是精确地址」又必须被钉住(混用聚合键正是曾经的缺陷)。
|
||||
type captureStore struct {
|
||||
hits []*db.HoneypotHit
|
||||
}
|
||||
|
||||
func (c *captureStore) Create(_ context.Context, hit *db.HoneypotHit) error {
|
||||
c.hits = append(c.hits, hit)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *captureStore) ListSince(context.Context, time.Time, int) ([]*db.HoneypotHit, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *captureStore) CountSince(context.Context, time.Time) (int64, error) { return 0, nil }
|
||||
|
||||
func (c *captureStore) TopSources(context.Context, time.Time, int) ([]*db.HoneypotSourceStat, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *captureStore) TopPaths(context.Context, time.Time, int) ([]*db.HoneypotPathStat, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *captureStore) DeleteBefore(context.Context, time.Time) (int64, error) { return 0, nil }
|
||||
|
||||
func (c *captureStore) DeleteOldestBeyond(context.Context, int) (int64, error) { return 0, nil }
|
||||
|
||||
func (c *captureStore) Compact(context.Context) error { return nil }
|
||||
|
||||
func (c *captureStore) Size(context.Context) (db.HoneypotSize, error) {
|
||||
return db.HoneypotSize{}, nil
|
||||
}
|
||||
|
||||
func (c *captureStore) LegacyCredentialColumns(context.Context) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *captureStore) PurgeLegacyCredentialColumns(context.Context) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -105,6 +105,23 @@ func newBanner() *autoban.Banner {
|
||||
HoneypotEnabled: conf.Security.EnableHoneypot,
|
||||
HoneypotThreshold: conf.Security.HoneypotThreshold,
|
||||
HoneypotDuration: time.Duration(conf.Security.HoneypotBanMinutes) * time.Minute,
|
||||
|
||||
// 建封日志:封禁表是纯内存、外部完全看不见,而线上出现过
|
||||
// 「邮件说封了、实际还能访问」—— 当时没有任何记录能说明封的是哪个键、
|
||||
// 到什么时候解除,排查只能靠猜。键与 IsBanned 的判断口径完全一致
|
||||
// (shared.LimitKey,IPv6 折成 /64),日志与判定不会各说一套。
|
||||
OnBan: func(key string, duration time.Duration, reason string) {
|
||||
reasonText := "敏感路径探测"
|
||||
if reason == autoban.ReasonHoneypot {
|
||||
reasonText = "蜜罐提交凭据"
|
||||
}
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"ban_key": key,
|
||||
"reason": reasonText,
|
||||
"duration": duration.String(),
|
||||
"expires_at": time.Now().Add(duration).Format("2006-01-02 15:04:05"),
|
||||
}).Warn("已自动封禁来源(内存态,重启进程即解除;键为聚合口径,IPv6 为 /64)")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user