规则裁剪 20% 的取舍:判据、演练与代价
起因:付费的是加载,不是命中
先把数字摆出来(2026-10-13 实测):
单文件 suricata.rules:69,080 行 / 53,122 条 alert 规则(44 MB)
近 24h 实际命中:只有 1 个签名 —— ET INFO Python-urllib(约占该组 97%)
读法很直接:规则文件 44 MB、五万多条 alert 规则, 而它们几乎每天都在"被加载、被匹配",然后什么也不报。
- 加载开销:每次启动、每次热加载;
- 匹配开销:每一个包都要过一遍;
- 运维开销:真出告警时,人要在噪声里找那条真的。
所以裁剪的目标不是"规则越少越好",而是:
把"从不命中的 Informational 噪声"摘出去,同时保证在用的检测一个不丢。
判据:三条原则,一条都不能松
裁剪原则(三条同时满足才进候选):
- 24h 无命中;
- 不在演练覆盖集里(演练要用到的签名一律保留);
- 非高危类别。
这里有一个我犯过的错,必须写出来
我用过一版错误判据:按消息文本搜关键词(“Malware”/“Trojan”/“exploit”)。
这是错的,原因很实在:消息文本里出现这些词,不代表它属于高危类别。
实测里消息文本带这些字样的 21 条,逐条看下来全是 ET INFO / FILE_SHARING 低危规则。
正确判据是按 classtype 核。 候选集的 classtype 分布实测是:
bad-unknown 6553
misc-activity 3498
policy-violation 434
external-ip-check 137
not-suspicious 53
→ 无一条属真正的 MALWARE / EXPLOIT / TROJAN / C2 规则类
一般化教训:用结构化字段做安全判断,不用文本匹配。 文本匹配看起来更快,但它会把"描述里提到这个威胁"和"这条规则在防这个威胁"混为一谈 —— 在裁剪这种不可逆动作上,这个混淆的代价是全盲。
怎么证明"没裁错":三类证据
裁剪是一类很难回滚得当的操作(规则删了、告警少了,你很难知道少了什么)。 所以证据必须是结构化的、可复现的,不能是"我看了一眼没问题"。
证据一:生产唯一在响的签名还在
sid 2013031(ET INFO Python-urllib) 仍在 ✓
另 2001219(ET SCAN SSH)/ 2002910 / 2010935 / 2001569 / 2013028 全部保留 ✓
证据二:阳性对照 —— 重启后它照常在报
这一步是最关键的一步,因为它验证的是"运行时真的用上了新规则集":
2026-10-13 09:47:26 CST:重启 suricata 容器加载裁剪后的规则(维护窗口内,已获批准)
· 容器内规则 58,405 行 / alert 42,447 条 ✓
· 解析/配置错误 0 条 ✓
· 阳性对照:生产里持续命中的 ET INFO Python-urllib (sid 2013031) 重启后照常在报
(fast.log 01:48:51 / 01:48:56 UTC 两条,节奏与裁剪前一致)→ 裁剪未伤及在用检测 ✓
· eve.json 持续写入 ✓
为什么说是"阳性对照"而不是"检查配置":配置正确 ≠ 运行时加载了。 只有在新规则生效之后看到那条已知会命中的告警继续出现, 才能证明"引擎确实在用这份新规则集,而且它还能工作"。
一个负面对照同样重要:如果重启后它不报了, 那你分不清"是裁剪裁坏了"还是"这段时间真的没流量"。 没有阳性对照的验证等于没有验证。
证据三:新旧引擎对照演练,丢失 = 0
实施方另做 4 次对照演练(本地轮 / srv1 轮 × 旧引擎 / 新引擎):
基线命中集 {2228000, 2260000} 在裁剪后【逐条复现、丢失 = 0】
indexer 侧独立复核:2013031×96 | 2228000×24 | 2260000×12 | 2001219×1(吻合)
裁剪后 24h 命中集合仍为 58 个,且与裁剪前逐项一致
最后一行是这次裁剪里我最看重的数字:58 个命中项,逐项一致。 不是"总数差不多",而是集合相等 —— “总数差不多"可以掩盖"丢了两个、多了两个”,集合相等不行。
结果
alert 规则:53,122 → 42,447(-10,675,约 -20%)
ET INFO 类:5,341 → 1,150 条
备份:/opt/suricata/backup/suricata.rules.bak-20261013-093222(原始 69,080 行)
候选留档:prune-candidates-*.tsv(1.1 MB)/ prune-candidate-sids / prune-keep-sids
“候选留档"这一项我想单独强调:裁剪决策的全过程(哪些进了候选、哪些明确保留) 都留成了文件。理由是:半年后有人问"为什么这条规则没了”,答案应该在仓库里,不在我脑子里。
代价:被裁掉的是弱信号,不是噪声
诚实写清另一面。被裁掉的确实包含一些弱信号,例如:
- “HTTP 请求
.suTLD 常与恶意软件相关” - “可执行文件从
/tmp目录提供” - “Windows 98/3.1 User-Agent 检出”
它们 24h 从未命中,但"从未命中"不等于"永远不会命中"。 取舍理由是:这几类信号在本环境里从未出现过,却在每次加载/匹配上都要付费。
有三条缓解措施让这个取舍可控:
- 备份在:
suricata.rules.bak-20261013-093222是裁剪前的完整 69,080 行; - 候选集留档:
prune-candidate-sids记录被裁的 sid,可按需只加回特定的一批; - 不是永久决定:将来需要这类信号,恢复或按需加回都行。
一个诚实的判断:这次裁剪降低的是"在本环境里永远不命中的规则的持有成本", 代价是"如果出现这类攻击、而它只在被裁的规则里有特征,我们会漏"。 这个代价我认为值得,但它不是零 —— 写在这里,不粉饰。
三个必须记住的操作事实
裁剪过程中撞到的三个坑,全部写进文档,避免下一个人重新踩:
① 真实检测空窗 ≈ 41–42 秒,不是 5–10 秒。
(Signal Received → Engine started。)而且本配置关闭了 console 日志,
docker logs 里看不到 “Engine started” —— 必须以 suricata.log 的时间戳为准。
今后在维护窗口里重启 suricata,按 45 秒空窗规划。
② 不要在 512 MiB 的 cgroup 里执行 docker exec suricata suricata -T。
那会在同一个 cgroup 内再起一个 450–485 MiB 的引擎,与在线引擎叠加有 OOM 风险
(实测侥幸未触发)。
→ 校验规则请用一次性独立容器(把 rules + yaml 挂进去跑 -T),或先停在线引擎。
③ 09:47:15 那次"非实施方发起的 docker-API 重启"是我做的(为了加载裁剪后的规则), 不是 OOM、不是异常。 这一条是"把动作写进日志"的价值:否则半年后有人看到这条重启记录, 会以为当时出了故障。
一个未决项:那个从未生效的 threshold.conf
这一条我在蜜罐那篇里也提过,但值得在这里再写一遍,因为它是一个典型的"验证不完整":
compose 只挂载 etc/suricata.yaml、rules、logs
→ threshold.conf 不在挂载之列
→ 它只是镜像层里一个 0 字节文件(/etc/suricata/threshold.conf,Oct 12 16:08)
也就是说:“sid 2001219 阈值抑制"从未真正生效。
当初只把 yaml 的 threshold-file: 指过去,没把抑制规则写进那个文件,
而"文件内容"这一层当时没验。
**修法(若启用)**在这里写清,站主确认前不动:
- 把抑制规则写到已挂载的目录(如
/opt/suricata/rules/threshold.conf); - yaml 的
threshold-file指向/var/lib/suricata/rules/threshold.conf; - 备份 → 改 → 用一次性容器跑
-T校验(见上面第 ② 条); - 重启(按 41s 空窗规划);
- 确认日志里
Threshold config parsed: N rule(s) found的 N > 0。
第 5 条是这个修法的核心:放行条件必须是"引擎自己说它读到了几条阈值规则”, 而不是"我把文件写对了"。
小结:裁剪的纪律清单
| 步骤 | 必须做到 |
|---|---|
| 定判据 | 用结构化字段(classtype),不用消息文本关键词 |
| 保留集 | 演练覆盖集 + 在用签名,一律先保留 |
| 阳性对照 | 重启后,已知会命中的签名必须继续出现 |
| 集合对照 | 对比"逐项集合",不是"总数差不多" |
| 留档 | 备份 + 候选 sid + 保留 sid,全部入库 |
| 放行条件 | 引擎的退出码 / 引擎自己打印的计数,不是人工判断 |
| 操作事实 | 空窗时长、cgroup 内存限制、谁按了重启,全部写下来 |
一句话:安全上的"减法"比"加法"更需要证据 —— 你删掉的东西不会再来告警提醒你它被删了。
本文的规则条数、sid、classtype 分布、耗时与文件路径均来自本项目的执行记录与 RUNBOOK.md,未做美化。

