用纯开源方案搭建生产级 SOC:Wazuh + DFIR-IRIS + 自研集成层实战记录
一份完整的现场报告:我们如何为一家中型企业用 Wazuh、DFIR-IRIS 和一个自研的 Python 中间层搭建出真正可用的安全运营中心 — 哪些设计有效、哪些坑踩过、以及那些真正影响成败的工程决策。
凡是给商用 SIEM 或 SOC 平台询过价的人都明白一件事:仅是 license 费用,往往就超过了使用这套平台的分析师的工资总和。这就是开源 SOC 方案吸引人的原因。但真正的难点不在工具本身,而在于让这些工具像一个统一的产品一样协同工作 — 大多数项目在这里就停下了。
本文是一份来自一线的工程笔记。没有 SaaS、没有云锁定,所有组件都跑在客户自己机房的 Docker 上,软件总成本为零。
特别是在中国境内运营、需要满足《网络安全法》、《数据安全法》、《个人信息保护法》(PIPL) 以及网络安全等级保护 2.0 (等保 2.0) 要求的企业,日志在本地留存、保留可审计的处置链路,已经不是技术选择题,而是合规底线。
整体架构一览
| 层 | 工具 | 职责 |
|---|---|---|
| 检测 | Wazuh 4.x | 日志收集、事件解码、基于规则的告警触发 |
| 案件管理 | DFIR-IRIS | 分析师做研判、跟踪处置流程的工作台 |
| 集成 | SOC Integrator (FastAPI) | 把所有组件串起来的中间层 |
| 威胁情报 | VirusTotal + AbuseIPDB | 在告警生成时附加 IOC 信誉数据 |
| 呼叫 | PagerDuty | 凌晨叫醒值班分析师 |
| 自动化 | Shuffle SOAR | IOC 相关的工作流自动化 |
| 日志源 | FortiGate, Windows AD, VMware ESXi, Sysmon | 实际被监控的系统 |
各组件之间的数据流如下:
flowchart TD
A["FortiGate / Windows AD / VMware / Sysmon"] -->|"syslog / agent"| B["Wazuh Manager"]
B -->|"解码事件 + 告警"| C["Wazuh Indexer"]
C -->|"每5秒轮询"| D["SOC Integrator (FastAPI)"]
D -->|"丰富数据"| E["VirusTotal + AbuseIPDB"]
D -->|"创建告警"| F["DFIR-IRIS"]
D -->|"呼叫值班"| G["PagerDuty"]
D -->|"触发流程"| H["Shuffle SOAR"]
F -->|"分析师研判"| I["KPI 仪表板"]
开源 SOC 工具链很少有人提的一个真相 — 单看每个工具,质量都不差,但它们并不是设计成"一个产品"协同工作的。集成层才是把这一切捏合成一套连贯系统的关键。
第一部分 — Wazuh: 从通用检测到贴合自身环境的检测
Wazuh 出厂自带约 3,500 条规则,覆盖了暴力破解、常见恶意软件、可疑进程等"标准"场景。但通用规则只能抓通用威胁。要检测真正发生在 你自己环境 中的事件 — 你的特权账户、你的 IP 段、你独有的异常模式 — 必须自己写规则。
自定义规则的组织方式
我们按检测设计文档(经客户确认)中的用例附录来分文件:
wazuh_cluster/rules/
soc-a1-ioc-rules.xml # DNS / 威胁情报命中
soc-a2-fortigate-fw-rules.xml # FortiGate 防火墙事件
soc-a3-fortigate-vpn-rules.xml# VPN 隧道事件
soc-a4-windows-ad-rules.xml # Windows 认证
soc-b1-vmware-rules.xml # vCenter / ESXi
soc-b2-logmon-rules.xml # 日志缺失监控
soc-c1-c3-rules.xml # 多阶段相关性检测
soc-ioc-cdb-rules.xml # 威胁情报 CDB 查找
每条规则都有两套版本:仿真版 (在 UAT 环境安全触发,ID 段 100xxx) 和 生产版 (按真实流量调优,ID 段 110xxx)。两套放在同一个文件里,一次 rule_test 就能验证两条路径。
<!-- 仿真版: 通过模拟器 JSON 事件触发 -->
<rule id="100341" level="10">
<if_sid>110350</if_sid>
<description>A4-01 [SIM] Windows 特权账户认证失败</description>
<group>soc_sim,a4,windows,auth,</group>
</rule>
<!-- 生产版: 匹配真实 Windows 事件日志字段 -->
<rule id="110341" level="10">
<if_group>windows</if_group>
<field name="win.eventdata.targetUserName" type="pcre2">(?i)(admin|adm_|svc_|sa_|-adm)</field>
<field name="win.system.eventID">^4625$</field>
<description>A4-01 [PROD] 特权账户认证失败</description>
<group>soc_prod,a4,windows,auth,</group>
<mitre><id>T1110</id></mitre>
</rule>
容易被忽略的细节: location 字段
当一台设备直接向 Wazuh 发 syslog (没有装 agent),Wazuh 会把 agent.name 设成 wazuh.manager,把源 IP 存在 location 字段里。这点文档里几乎没写,我们也是踩过几小时坑才弄明白。
VMware ESXi 只能 syslog,所以必须在 location 上按 IP 过滤:
<rule id="110401" level="12">
<if_group>vmware</if_group>
<field name="location" type="pcre2">^172\.16\.0\.(107|108|109|110)$</field>
<match>Login failure</match>
<description>B1-01 [PROD] vCenter 登录失败</description>
<mitre><id>T1110</id></mitre>
</rule>
不加这个过滤的话,任何 发送 "Login failure" 字符串的设备都会触发这条规则。把范围限定到这 4 台 ESXi 主机后,误报基本消失。
与其和内置规则对抗,不如继承它
ESXi 的 SSH 事件由 Wazuh 的 sshd 解码器处理,而非 vmware 解码器。所以 if_group=vmware 抓不到。正确做法是从已经触发的内置规则继承:
<rule id="110404" level="10">
<if_sid>5715</if_sid> <!-- 内置 SSH 成功规则 -->
<field name="location" type="pcre2">^172\.16\.0\.(107|108|109|110)$</field>
<description>B1-04 [PROD] ESXi SSH 登录成功 — 确认是否授权</description>
<mitre><id>T1021.004</id></mitre>
</rule>
规则 5715 已经把事件解析好了,我们只需要把范围收窄到目标主机。
通过 CDB 列表实现威胁情报查找
Wazuh 的 CDB(常量数据库)以 O(log n) 复杂度在平铺文件里查值。我们维护三份列表 — 恶意 IP、恶意域名、恶意哈希 — Integrator 每 4 小时刷新一次,从 VirusTotal 和 AbuseIPDB 拉取最近 30 天确认命中的 IOC。
<rule id="110600" level="13">
<if_sid>22101</if_sid>
<list field="data.srcip" lookup="match_key">etc/lists/malicious-ioc/malicious-ip</list>
<description>FortiGate 源 IP 命中威胁情报列表</description>
<mitre><id>T1071</id></mitre>
</rule>
让我们浪费几小时的 Docker 坑
Wazuh 官方镜像把规则放在挂载到 /var/ossec/etc 的命名卷里。如果你试图用 docker cp 或 sed -i 改规则文件,要么静默失败,要么报 "Device or resource busy" — 命名卷的 inode 优先级更高。
唯一能在不重启容器的前提下原地更新规则的可靠方法,是从容器内部用 Python 的 open().write() 写文件:
docker exec wazuh.manager python3 -c "
with open('/var/ossec/etc/rules/soc-a4-windows-ad-rules.xml', 'w') as f:
f.write('''<group name=\"soc_mvp,...\">...</group>''')
"
docker exec wazuh.manager /var/ossec/bin/wazuh-control reload
任何会触碰 Wazuh 生产规则的 CI/CD 流水线都应该把这一点写进规范里。
第二部分 — Integrator: 让一堆工具变成一个产品的那层
这是把 Wazuh、IRIS 和其他工具从"四个独立系统"变成"一套整体方案"的核心。它是一个 FastAPI 服务,负责 5 件事。
1. 告警同步
每 5 秒,Integrator 查询 Wazuh Indexer,拉取匹配生产规则 (rule.id:[110301 TO 110602]) 的新事件,做归一化,然后在 IRIS 里创建对应告警。用复合键去重,避免同一事件淹没队列。
2. IOC 富化
对于威胁情报类规则,Integrator 在 创建 IRIS 告警之前 就调 VirusTotal 和 AbuseIPDB。分析师打开告警的瞬间就能看到 — 信誉分、威胁分类、最后命中日期。不用切标签页,不用复制粘贴。
3. 多阶段相关性检测
有些攻击只有把多个事件放在一起看才能发现。Wazuh 规则是无状态的,所以我们在 PostgreSQL 里跑了一个相关性引擎。最经典的例子是"不可能旅行" — 同一个用户从两个物理上不可能在已过时间内到达的城市做了 VPN 登录:
async def _detect_impossible_travel(self, event: dict) -> dict | None:
# ... 查这个用户的上一次登录 ...
distance_km = haversine(loc1, loc2)
min_hours = distance_km / 900 # 商用航班最大速度
actual_hours = (ts2 - ts1).total_seconds() / 3600
if actual_hours < min_hours:
return {"confirmed": True, "distance_km": distance_km, ...}
如果北京 10:00,法兰克福 11:30,那是物理上不可能的。Integrator 确认 C1 检测,在 IRIS 里生成一条高严重度告警,并把两次登录事件作为附件挂上。
4. 分组去重
一次密码喷洒攻击就能在一小时内产生数千条 Windows 4625 事件。如果每条都变成 IRIS 告警,队列直接废掉。所以我们按 (rule_id, user, host) 分组,每条规则配自己的冷却时间:
_GROUP_DEDUP_RULES: dict[str, int] = {
"110341": 2, # 特权账户认证失败 — 2 小时
"110342": 2, # 服务账户认证失败 — 2 小时
"110344": 4, # 公网 IP 认证失败 — 4 小时
"110347": 4, # runas / 权限假冒 — 4 小时
"110359": 1, # 密码喷洒 — 1 小时
}
第一条匹配 (rule_id, user, host) 的事件创建 1 条 IRIS 告警。冷却期内的后续事件只更新 last_seen 并写入 incident_events 表用于后续取证,不重复创建告警。关键点是:抑制逻辑是显式且可审计的 — 后续可以查到哪些事件被抑制了、为什么被抑制。这一点在等保 2.0 测评和 PIPL 合规审计时尤其重要。
5. 基于 SLA 的 KPI 跟踪
每条告警都有按严重度计时的 SLA:
| 严重度 | SLA 目标 |
|---|---|
| 紧急 (Critical) | 1 小时 |
| 高 (High) | 4 小时 |
| 中 (Medium) | 8 小时 |
| 低 (Low) | 24 小时 |
IRIS 仪表板每条告警显示一个实时进度条 — 绿色代表在 SLA 内、黄色代表超过 75%、红色代表已逾期。管理层拿到的是诚实的数字,分析师看到的是真正紧急的事件。
每条告警还附带一份从 YAML 在启动时加载的处置指引:
guidelines:
110346:
use_case: "A4-06 — 公网 IP 认证成功"
steps:
- "立即识别用户与源 IP"
- "确认是否为预期内行为(远程办公、出差)"
- "检查会话内是否有特权操作"
- "若未授权: 强制登出、重置凭证、边界封禁 IP"
分析师打开告警就能看到处置剧本,不需要去 wiki 找运维手册。
为什么不用 Wazuh 自带的 Active Response?
Wazuh 有个 "Active Response" 框架,可以在规则匹配时执行脚本。我们没用。四个原因:
- 状态 — 相关性检测需要历史数据。Active Response 是无状态的
- 异步 I/O — 在 async Python 里并行调用 VirusTotal 和 AbuseIPDB 很自然,在 shell 里就很痛苦
- 可测试性 — Integrator 有完整的 REST API。任意事件都可以通过
POST /monitor/wazuh/ingest重放,每一步决策都能复盘 - 解耦 — IRIS、PagerDuty、Shuffle 都可替换。Wazuh 完全不知道它们的存在
27 秒仪表板事件
刚上线时,IRIS KPI 仪表板加载要 27 秒。直接调端点很快(128 毫秒),但走代理链就慢。
根本原因是每次数据库调用都用 psycopg.connect() — 这是一个同步阻塞调用(每次约 34 毫秒,主要花在 TCP 握手和认证上)。Auto-sync 每 5 秒处理约 437 个事件,每个事件 2 次数据库调用:
437 个事件 × 2 次 DB 调用 × 34 毫秒 = 约 30 秒事件循环被堵
这段时间里到达的任何 HTTP 请求都得排在 sync 后面。修复方法是把"每次调用建一个新连接"改成"持久连接池":
# 修复前: 每次都建新 TCP 连接
@contextmanager
def get_conn():
with psycopg.connect(db_dsn(), ...) as conn:
yield conn
# 修复后: 从连接池借连接 — 每次约 1 毫秒
_pool: ConnectionPool | None = None
def init_pool() -> None:
global _pool
_pool = ConnectionPool(db_dsn(), min_size=2, max_size=10, ...)
@contextmanager
def get_conn():
with _pool.connection() as conn:
yield conn
仪表板加载时间: 27 秒 → 0.2 秒。教训: 任何按请求做数据库读写的 FastAPI 服务,从第一天就用异步连接池。
闭合反馈环
Integrator 确认多阶段检测(比如"不可能旅行")后,会把一条结构化的 syslog 消息回送给 Wazuh。Wazuh 有一条规则匹配这条消息,在自己的仪表板上触发一条 level-15 告警。这样,在 Wazuh 上工作的分析师和在 IRIS 上工作的分析师都能看到同一个事件。哪个工具都不是"主"的。
第三部分 — DFIR-IRIS: 不 fork 也能定制
DFIR-IRIS 出厂就很扎实 — 告警、案件、IOC、时间线、报告都齐。我们做了两处扩展,上游代码只动了一行。
用 Flask Blueprint 做 KPI 仪表板
IRIS 是 Flask 应用,加新页面很直接 — 写一个 Blueprint,注册一次就行。
@kpi_dashboard_blueprint.route('/kpi-dashboard')
@ac_requires(no_cid_required=True)
def kpi_dashboard(caseid, url_redir):
return render_template('kpi_dashboard.html', csrf_token=generate_csrf())
@kpi_dashboard_blueprint.route('/kpi-dashboard/api/alerts')
@ac_api_requires(Permissions.alerts_read)
def proxy_list_alerts():
content, status, _ = _soc_get('/iris/alerts', request.args)
return Response(content, status=status, content_type='application/json')
前端用 Alpine.js — 体量小、不需要构建步骤、轮询代理端点,渲染带 SLA 计时器和一键分配的实时告警列表。
结构化告警备注
不用自由文本,每条 IRIS 告警都带一份固定结构的 JSON 备注:
{
"rule": {
"id": "110344",
"description": "公网 IP Windows 认证失败",
"mitre": ["T1110"]
},
"asset": {
"hostname": "FPFTPSRV02",
"ip": "172.16.10.50",
"os": "Windows"
},
"network": {
"src_ip": "91.202.x.x",
"dst_ip": "172.16.10.50",
"protocol": "TCP"
},
"guideline": {
"use_case": "公网 IP 认证失败",
"steps": ["识别源 IP 和地理位置", "..."]
}
}
UI 上人看得懂,过滤和报告时机器也能解析。分析师可以按 rule.id 或 asset.hostname 检索;管理者可以按 MITRE 技术分组生成趋势报告 — 这对等保 2.0 的安全运维报表也是直接可用的输出。
怎么定制 IRIS 又不 fork 它
直接 fork 项目、改源码、上线 — 这是最容易的诱惑。代价是 6 个月之后,上游发新版,你的 fork 已经漂移到合不回去了。
我们的做法:
- 所有自定义 Blueprint 放在 bind-mount 进容器的目录里
- 前端单独编译,作为静态包注入
- 上游
__init__.py里只动一行,用来注册我们的 Blueprint
那一行就是 IRIS 升级时唯一需要重新打的补丁。其他改动都是增量的、不侵入的。
第四部分 — 运营层面的经验
磁盘消耗会让你意外
logall=yes 会把所有解码事件都归档,不只是触发告警的那部分。做取证深挖时很有用,但对磁盘空间不友好。一次例行的 VMware 排查就把磁盘使用率推到 86%。现在我们默认关掉,只在做针对性调查时临时开启:
<global>
<logall>no</logall>
<logall_json>no</logall_json>
</global>
从 PIPL 合规角度看,日志保留时长应当是一项明确的策略,而不是磁盘容量的副产品。
覆盖度 vs 告警疲劳是可调参数,不是非此即彼
抑制吵闹规则只是表面解。更好的方案是上面提到的分组去重模式 — 每个事件仍然被记录,但每个冷却窗口里只有第一条会创建告警。覆盖度不丢、审计链不断、分析师队列保持可用。
监控你自己的 SOC
Integrator 每 2 分钟自检一次它自己的依赖 — Wazuh Manager、Wazuh Indexer、IRIS、Shuffle、PagerDuty。任何一个不在线就在 IRIS 里以 system-health 类别创建告警。分析师在同一个队列里就能看到基础设施故障和安全告警,不需要再装一套监控工具。
邮件依然是最可靠的兜底通知
紧急 IRIS 告警会向 SOC 团队和备用邮箱发邮件,内容包含告警标题、严重度、资产、IRIS 直链。仪表板会挂,收件箱不会。
数字
| 指标 | 数值 |
|---|---|
| 自定义 Wazuh 规则 | 86 条 / 8 个文件 |
| 已在生产触发的规则 | 64 条中的 17 条 |
| 处理过的 IRIS 告警 | 91,000+ |
| Auto-sync 周期 | 每 5 秒 |
| IOC 列表刷新 | 每 4 小时 |
| KPI 仪表板加载 (修复前) | 27 秒 |
| KPI 仪表板加载 (修复后) | 0.2 秒 |
| 数据库调用开销 (修复前 / 后) | 34 毫秒 / 1 毫秒以下 |
重做一次会改的事
第一天就上异步数据库: 在 FastAPI 应用里用同步版 psycopg.connect() 是埋好的性能炸弹。在写第一个 repository 之前,就用 psycopg_pool.AsyncConnectionPool。
Wazuh 规则测试纳入 CI: rule_test API 接受原始日志行,返回触发的规则。把它包进 pytest fixture 就能在上线前发现规则冲突。
仿真和生产用不同的 syslog 端口: 我们把两边都塞到 514 端口,导致必须维护 soc_sim/soc_prod 两套规则。换不同端口会简洁得多。
总结
完全跑在开源软件上的生产 SOC,不仅是可行的,而且是扎实的工程实践。但开源工具本身不构成产品。集成层 — 那个没人拿去做营销的部分 — 才是真正的 SOC 所在。
如果你正在评估商用 SIEM,而 license 报价让你坐立不安,这套技术栈是一个货真价实的替代方案。难点不在工具,难点在"胶水"。
对于在中国境内运营、需要满足《网络安全法》、《数据安全法》、《个人信息保护法》(PIPL) 以及网络安全等级保护 2.0 (等保 2.0) 要求的企业,这套架构还有几个额外价值 — 日志全程在自有数据中心、抑制逻辑可被合规审计、不需要把敏感数据外送到境外云。
Simplico 专注基于开源构建生产级安全系统。如果您正在规划 SOC、升级现有检测体系,或试图摆脱 SIEM 的 license 重压,欢迎与我们联系。
Get in Touch with us
Related Posts
- How We Built a Real Security Operations Center With Open-Source Tools
- FarmScript:我们如何从零设计一门农业IoT领域特定语言
- FarmScript: How We Designed a Programming Language for Chanthaburi Durian Farmers
- 智慧农业项目为何止步于试点阶段
- Why Smart Farming Projects Fail Before They Leave the Pilot Stage
- ERP项目为何总是超支、延期,最终令人失望
- ERP Projects: Why They Cost More, Take Longer, and Disappoint More Than Expected
- AI Security in Production: What Enterprise Teams Must Know in 2026
- 弹性无人机蜂群设计:具备安全通信的无领导者容错网状网络
- Designing Resilient Drone Swarms: Leaderless-Tolerant Mesh Networks with Secure Communications
- NumPy广播规则详解:为什么`(3,)`和`(3,1)`行为不同——以及它何时会悄悄给出错误答案
- NumPy Broadcasting Rules: Why `(3,)` and `(3,1)` Behave Differently — and When It Silently Gives Wrong Answers
- 关键基础设施遭受攻击:从乌克兰电网战争看工业IT/OT安全
- Critical Infrastructure Under Fire: What IT/OT Security Teams Can Learn from Ukraine’s Energy Grid
- LM Studio代码开发的系统提示词工程:`temperature`、`context_length`与`stop`词详解
- LM Studio System Prompt Engineering for Code: `temperature`, `context_length`, and `stop` Tokens Explained
- LlamaIndex + pgvector: Production RAG for Thai and Japanese Business Documents
- simpliShop:专为泰国市场打造的按需定制多语言电商平台
- simpliShop: The Thai E-Commerce Platform for Made-to-Order and Multi-Language Stores
- ERP项目为何失败(以及如何让你的项目成功)













