蜜罐与诱饵端口:从「看不见」到「能检出」
先纠正一个常见误解
看到"多端口扫描没告警",第一反应通常是:规则不够,加规则。
在本环境里这个反应是错的。实测结论写在这里:
多端口端口扫描的"漏报",不是规则缺口,而是可见性缺口。
证据(2026-10-13 实测):
从 srv1 对 srv2 扫 8 个端口(22/23/25/80/443/3306/8080/9200,间隔 600ms):
· eve.json 里只出现 port 22 的 flow
· 其余 7 个端口的 SYN 在【云安全组】就被丢弃
· 传感器看不到的包,任何 Suricata 规则都检测不到
“看不见"和"没发生"在告警面板上长得一模一样 —— 这是安全运营里最贵的一类错觉。 所以这一篇的重点不是"怎么加规则”,而是怎么证明自己看不见。
怎么证伪"加规则就能解决"
这件事不能靠推理,要靠一次对照实验:
把规则目的地址放宽成
any之后确实会触发。 但命中的是10.0.0.10 -> 169.254.0.x的出站流量 —— 也就是说, 触发规则的是误报,而不是真的检测到了那次扫描。
这个实验的价值在于:它把"我加个规则就好了"这个想法当场证伪了。 如果不做这一步,接下来的动作就是往规则集里塞一条错误的规则 —— 既没补上可见性,还增加一串误报。
结论:不再新增"低速多端口扫描"规则。 扩大检测面的唯一正确做法是让更多端口对传感器可见(云安全组放行), 而那属于维护窗口 + 站主决策,不是我能自己拍的事。
那么扫描真的完全检不出来吗?
不是。要分清楚"哪一层的扫描是可见的":
| 目标 | 可见性 | 检测 |
|---|---|---|
| 非 22 端口的多端口扫描 | 不可见(安全组丢弃) | 任何规则都检不到 |
| SSH 层面的扫描 | 可见、可检测 | ET SCAN sid 2001219 |
所以对外的口径应该是"SSH 扫描可检出",而不是含糊的"我们做扫描检测"。 含糊的能力宣称最后都会在面试里被问倒。
误报抑制:让运维批量登录不再刷告警
可见性解决之后,紧接着是噪声问题。这里我踩了三个坑,全部固化进了脚本。
坑 1:suricata.yaml 是单文件 bind mount
sed -i ...
# cannot rename ... Device or resource busy
修法:必须就地覆盖写(read → 改 → write 同一个文件)。
坑 2:配置必须以两行开头
%YAML 1.1
---
把 threshold-file 插到最顶端会被 Suricata 拒绝:
The configuration file must begin with the following two lines
修法:插在 --- 之后。
坑 3:rule-files 追加新项要插到块末尾
不能紧跟 rule-files: 头行 —— 否则顺序变成"新项在前",语义就变了。
唯一的放行条件
改完必须以 suricata -T 的退出码作为唯一放行条件(exit=0 才加载),
失败立即回滚备份。脚本 scripts/apply-suricata-scan-tuning.sh 支持:
--check # 只校验当前配置(suricata -T)+ 打印 rule-files
--rollback # 回滚到最近一次备份并校验
(无参数) # 备份 → 写阈值 → 行级插入 threshold-file
# → suricata -T 放行 → 热加载/重启 → 失败自动回滚
“配置改完必须用真实引擎校验” 和上一篇部署链的逻辑一样: 声明不算数,退出码才算数。
一个必须自己承认的坏消息
写过的东西如果只是"写在那里",它就不算生效。这一条我自己撞上了:
⚠️〔2026-10-13 核实:该抑制从未真正生效 ——
threshold.conf未被 compose 挂载且为 0 字节〕
也就是说:脚本写了、--check 跑了、文档也记了,但那份阈值文件从来没被容器读到过。
这件事的意义比它本身大:它说明"我们做了误报抑制"这句话, 在核实挂载与文件大小之前,是一句没有证据的话。
修法与纪律:任何"已生效"的宣称,必须落到一个容器内可读的事实上 (文件存在 + 非空 + 被挂载 + 引擎校验通过)。这四件事缺一不可。
蜜罐侧:我们能给出什么、不能给出什么
蜜罐(Cowrie)的数据来自 Wazuh 的规则描述,实测规则形状(2026-10-12,共 9352 条):
100100 蜜罐: 新的 SSH 连接来自 <ip> 3127
100104 蜜罐: SSH 客户端版本 <ver> 来源=<ip> 3122
100102 蜜罐: 攻击者"登录成功" 用户=<u> 口令=<p> 来源=<ip> 3113
100101 登录失败 5
关键点:值直接写在 rule.description 里,
所以在 OpenSearch 里没法直接做 terms 聚合,只能在 Java 侧用正则抽取再聚合。
哪些真的拿到了
| 维度 | 状态 |
|---|---|
| 来源 IP(掩码到网段) | ✅ 真实计数 |
| 尝试账号 / 口令 | ✅ 真实(但公开面只给结构,见下) |
客户端指纹(如 SSH-2.0-Go) | ✅ 真实 |
| 连接 / 登录 / 登录失败计数 | ✅ 真实 |
| 命令时间线 | ❌ commandsAvailable=false |
最后一行要单独说:蜜罐里敲过的命令拿不到,因为 Wazuh 侧没有命令执行规则 —— 数据源里根本没有这一维度。
所以页面上的处理是:这一格保持「数据源未接入」,不编造命令内容。 一个"看起来像命令时间线"的东西,比空着更糟 —— 它会让人以为这条链路是通的。
公开面为什么只给口令结构,不给完整口令
这条是本篇最实用的一段。蜜罐真实捕获到的口令是敏感数据: 它可能对应攻击者正在使用的弱口令字典, 也可能(更糟)是误填进来的真实凭据。
所以公开面不下发完整口令,只给聚合结构与掩码示例:
| 给出 | 不给 |
|---|---|
| 长度分桶(≤6 / 7–9 / ≥10) | 完整口令串 |
| 是否与用户名相同 | |
| 字符构成(纯数字 / 字母+数字) | |
掩码示例,如 q****t | |
| 客户端指纹、来源网段 | 完整来源 IP |
实现上这是"从真实榜单现场归纳",不是预置的假分布:
前端拿到的是服务端给的 topCredentials(真实计数),
长度分桶与"是否与用户名相同"是从这份真实榜单现算的。
这样既不泄露原文,也没有编一个好看的分布图。
一个检测能力的取舍(写在明面上)
规则裁剪那件事我单独写在 另一篇里。这里只说和"可见性"直接相关的部分:
- 不做:为不可见端口新增规则(会产生误报,见上文对照实验);
- 不做:为"运维批量登录"放宽 SSH 扫描规则(那会真的丢掉扫描告警);
- 做:阈值抑制(按
by_src计数),并且用引擎退出码放行; - 做:把"哪些端口可见"写进文档,让下一个人不用重新做一遍对照实验。
小结:把"看不见"和"没发生"分开
这一篇的所有内容可以压成三条纪律:
- 告警为空时,先问"是没发生,还是看不见" —— 证明方式是对照实验(放宽规则看触发的是什么流量),不是推理。
- 配置生效与否,用引擎/容器的事实判断(
suricata -T退出码、文件非空、 被挂载),不用"我改过了"判断。 - 拿不到的东西就写"未接入" —— 命令时间线如此,完整口令如此。 一个假的占位比一个诚实空位危险得多。
本文的端口列表、规则 sid、报错原文、规则计数与字段名均来自本项目的 RUNBOOK.md 与实测输出;本文写作日期为 2026-10-13。

