SOCをゼロから構築する:Wazuh + IRIS-web 現場レポート
Wazuh 4.x、IRIS-web、自社開発のFastAPIインテグレーターを用いてSecurity Operations Centerをゼロから構築した3週間の実録。検知ルール、アラートパイプライン、IOCエンリッチメント、そしてアーキテクチャ図には決して登場しないインフラのバグまで、commitの履歴をたどりながら振り返る。
スタック: Wazuh 4.x · IRIS-web · soc-integrator (FastAPI) · OpenSearch · Docker Compose · VirusTotal API · AbuseIPDB
flowchart TD
subgraph "Log Sources"
A["Windows Agent"]
B["FortiGate Syslog"]
C["Simulator Scripts"]
end
subgraph "Wazuh"
D["Wazuh Manager"]
E["Decoders + Rules"]
F["OpenSearch Indexer"]
end
subgraph "soc-integrator (FastAPI)"
G["Alert Poller"]
H["Severity Filter"]
I["IOC Enricher"]
J["WazuhSyslogAdapter"]
K["Webhook Receiver"]
L["Email Notifier"]
end
subgraph "External Threat Intel"
M["VirusTotal"]
N["AbuseIPDB"]
O["Feodo / URLhaus / ThreatFox"]
end
subgraph "IRIS-web"
P["IRIS Alerts"]
Q["Cases + Triage"]
R["Outbound Webhook"]
end
A --> D
B --> D
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
I --> M
I --> N
O --> I
I --> P
P --> Q
Q --> R
R --> K
K --> L
J --> D
I --> J
第1週:白紙からのスタート(3月13〜16日)
SOCプロジェクトはいつも同じところから始まる。点滅するカーソルと、検知しなければならない脅威シナリオのリスト。
私たちのケースでは、社内仕様書の形で届いた。3つの付録に分けられたユースケース集だ。
- 付録A — WindowsおよびActive Directoryの悪用
- 付録B — ネットワーク・ファイアウォールイベント(FortiGate)
- 付録C — アイデンティティ:impossible travel、credential abuse、管理者によるlateral movement
最初のcommitは地味なものばかりだった。multipart依存関係の追加。3つの付録用サンプルログファイルのcommit――本番環境に近いシステムから収集したファイアウォール、認証、Windowsイベントデータ137行分。そしてpass.txtがリポジトリに追加された。初期SSHセッションの認証情報メモとテスト出力が書かれたスクラッチパッドだ。
3月16日までに、最初の本物のマイルストーンが完成した。test-firewall-syslog.pyスクリプトだ。このスクリプトはFortiGate形式のUDP syslogパケットをWazuhのポート514へ送信し、10種類のシナリオをカバーする。
Docker NATの問題
--via-dockerフラグはほぼ即座に追加された――2日目のことだ。このフラグがないと、すべてのパケットがホストIPではなくDockerゲートウェイIPをsource IPとしてWazuhへ届く。source IPによるルールマッチングがまったく機能しなくなる。このフラグを使うと、パケットがホストのネットワークスタック経由でルーティングされ、Wazuhが正しい送信元を認識できる。
Docker内でWazuhシミュレーションを構築していてsource-IPルールがfireしない場合、これが原因だ。
flowchart TD
A["test-firewall-syslog.py"]
B{"--via-docker flag?"}
C["Packet exits via Docker NAT"]
D["Wazuh sees Docker gateway IP"]
E["Source-IP rules never match"]
F["Packet exits via host network"]
G["Wazuh sees real host IP"]
H["Source-IP rules match correctly"]
A --> B
B -- "No" --> C
C --> D
D --> E
B -- "Yes" --> F
F --> G
G --> H
第2週:ルール、デコーダー、そしてORの罠(3月17〜22日)
検知エンジニアリングの大半は、正しいと思っていたものを書き直す作業だ。
3月17日、progress・progress-update・rule updateとタグ付けされたcommitが集中した。付録A・B・C全体で59本のシミュレーションルールが形になってきた――しかし何かがおかしかった。A2とA3のルールが、fireするべきでないタイミングでfireしていたのだ。
根本原因:multi-<match>によるORの罠
Wazuhのルール構文では、1つのルール内に複数の<match>タグを記述できる。多くのエンジニアはこれがANDロジックとして機能すると思い込む。しかしWazuh 4.xの特定のデコーダーチェーンでは、ORとして動作する。
action=denyかつlogid=13の両方が揃ったときだけfireするつもりで書いたルールが、どちらか一方の条件だけでもfireしてしまっていた。
<!-- 誤り:一部のデコーダーチェーンではORとして動作する -->
<rule id="100201" level="10">
<match>action=deny</match>
<match>logid=13</match>
<description>Firewall block — specific log ID</description>
</rule>
<!-- 正しい:単一のregexでANDを強制する -->
<rule id="100201" level="10">
<regex>action=deny.*logid=13|logid=13.*action=deny</regex>
<description>Firewall block — specific log ID</description>
</rule>
3月22日のcommitは修正を簡潔に記述している。
"fix A2/A3 rule OR-trap: replace multi-
<match>with single<regex>lookaheads."
この1行には、wazuh-logtestでサンプルイベントを投入し続け、無視するはずのinputにルールが反応するのを眺め続けた午後一杯の作業が凝縮されている。Wazuhルールが想定より広くfireする場合は、同一ルール内に複数の<match>タグがないか確認してほしい。
flowchart TD
subgraph "WRONG: multi-match behaves as OR"
A1["Incoming log event"]
B1["match: action=deny"]
C1["match: logid=13"]
D1["Rule fires if EITHER matches"]
A1 --> B1
A1 --> C1
B1 --> D1
C1 --> D1
end
subgraph "CORRECT: single regex enforces AND"
A2["Incoming log event"]
B2["regex: action=deny AND logid=13"]
C2["Rule fires only when BOTH match"]
A2 --> B2
B2 --> C2
end
第3週:WazuhとIRISの接続(3月23〜25日)
Wazuhルールがfireしてログファイルで確認できることと、アナリストがトリアージできるケース管理プラットフォームにアラートを届けることは、まったく別の問題だ。
soc-integratorパイプライン
答えはsoc-integrator――WazuhとIRIS-webの間に置くFastAPIサービスだ。
flowchart TD
A["Raw Security Event"]
B["Wazuh Manager"]
C["Decoder Chain"]
D["Rule Matching"]
E["OpenSearch Indexer"]
F["soc-integrator Poller"]
G{"Severity >= threshold?"}
H["Discard (noise)"]
I["Create IRIS Alert"]
J["IRIS-web Case Queue"]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G -- "No" --> H
G -- "Yes" --> I
I --> J
integratorの主な機能:
- N秒ごとにWazuh Indexer(OpenSearch)をポーリング
- 設定可能な重大度しきい値以上のアラートのみを転送(デフォルト:medium以上――それ以下はノイズとして扱う)
- アナリストがサービスを再起動せずにしきい値をリアルタイム調整できるGET/PUTエンドポイントを提供
- 完全なメタデータ付きのIRIS Alertを生成
3月23日、7ステップのエンドツーエンドテストスクリプトが、rawイベントからIRIS Alertまでのフローを確認した。
1日でタイムゾーンを3箇所修正
すべてのコンテナがUTCで動いていた。バンコク(ICT、UTC+7)のアナリストはタイムスタンプが7時間ずれた状態で見ていた。
- 通常コンテナ:Docker Composeのenvブロックに
TZ=Asia/Bangkokを追加 Go/scratchベースイメージ:ビルド時にタイムゾーンデータベースが削除されるため、volumeとして明示的にマウントが必要- おまけ:IRISナビゲーションバーにICT/UTC二重時計ウィジェットを追加
アナリストはすぐに気づいた。小さな変更だが、現場への影響は大きかった。
IOCエンリッチメントとShuffle SOARの廃止
3月24日まで、脅威インテリジェンスは手動だった――アナリストが疑わしいIPをブラウザで調べていた。新しいIOCパイプラインがそれを完全に置き換えた。
アドホック照会:
- VirusTotal――IP/ドメイン/ハッシュのレピュテーション確認
- AbuseIPDB――IPの不正利用履歴
バックグラウンドフィード取込:
- Feodo Tracker — C2インフラ
- URLhaus — 悪意のあるURL
- ThreatFox — IOC統合フィード
- MalwareBazaar — マルウェアハッシュ
WazuhのCDBリストファイル(malicious-ip、malicious-domains、malware-hashes)はWazuh APIを通じて再生成・ホットリロードされる。新規ルール110600〜110602がインラインCDBマッチングを担う。
flowchart TD
subgraph "Ad-hoc Lookups"
A["Suspicious IP / Domain / Hash"]
B["VirusTotal API"]
C["AbuseIPDB API"]
A --> B
A --> C
end
subgraph "Background Feed Ingestion"
D["Feodo Tracker (C2 IPs)"]
E["URLhaus (Malicious URLs)"]
F["ThreatFox (IOC Aggregator)"]
G["MalwareBazaar (Hashes)"]
end
subgraph "Wazuh CDB Hot-Reload"
H["malicious-ip list"]
I["malicious-domains list"]
J["malware-hashes list"]
K["Wazuh API reload trigger"]
H --> K
I --> K
J --> K
end
subgraph "Detection Rules"
L["Rule 110600: IP match"]
M["Rule 110601: Domain match"]
N["Rule 110602: Hash match"]
end
B --> H
C --> H
D --> H
E --> I
F --> H
F --> I
G --> J
K --> L
K --> M
K --> N
Shuffle SOARは完全に廃止した。 APIを直接呼び出す方式の方が高速でシンプルであり、別のワークフロープラットフォームを維持する必要もない。
プライベートIPの漏洩
テスト初期、integratorが192.168.x.xや10.x.x.xのアドレスをVirusTotalに送信していた。APIクォータを消費し、内部スキャントラフィックから429レートリミットエラーを発生させていた。
対処:外部エンリッチメントAPIを呼び出す前に、RFC1918およびループバック範囲を除外する。外部脅威インテリジェンスAPIへ送る前は、必ずプライベートIPをフィルタリングすること。
ディスク圧迫、何も表示しないダッシュボード、Syncノイズ
logall_jsonの惨事
開発中のデバッグ用に有効化していたlogall_json設定が、本番環境では1日あたり14GBのarchives.jsonを書き出していた。
対処:logall_jsonを無効化。OpenSearch ISMポリシーで30日後に古いインデックスを削除。コンテナ内OSレベルでログローテーションを追加。
何も表示しないダッシュボード
付録Cのダッシュボードはrule.id:1005*でフィルタリングしていた――開発時のシミュレーションルールIDだ。本番環境では実際の検知ルールが110xxx番台に存在する。そのためダッシュボードは実際のイベントに対して何も返さなかった。
対処:ルールIDフィルタリングからrule.groups:appendix_cへ切り替える。グループベースのフィルタリングはルールIDの変更に影響されない。
Syncフィルターのevent typeミスマッチ
Wazuh→IRISのsyncはevent_typeテキストフィールドでフィルタリングしていた。しかしWazuh agentが生成する実際のWindowsイベントにはそのフィールドが存在しない――シミュレーション時のアーティファクトだった。
対処:フィルターをルールIDのfrozensetとして再構築。明示的で決定的、コードレビューでも監査しやすい。
第4週:Webhook、メール通知、ループのクローズ(3月28日)
3月28日はプロジェクト中で最も密度の高い1日だった。
IRIS Webhookレシーバー
IRISがアラートを作成・更新したとき、オンコールのアナリストはどうやって知るのか?答えはsoc-integratorのwebhookレシーバーだ。IRISはモジュールシステムを通じてアウトバウンドwebhookをサポートしている。
flowchart TD
A["IRIS Alert created or updated"]
B["IRIS outbound webhook module"]
C["POST /iris/webhook (soc-integrator)"]
D["Parse alert payload"]
E["Enrich: entity name + event type"]
F["Resolve IRIS_EXTERNAL_URL"]
G["Format email body"]
H["smtplib send"]
I["On-call analyst inbox"]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
メール通知は1日の中で3回反復された。
| バージョン | 変更内容 |
|---|---|
| v1 | 「イベントが届いた」という素朴な通知のみ |
| v2 | 件名が「A1-02 Brute Force」に(「IRIS Event」ではなく) |
| v3 | 完全な情報:Alert ID、タイトル、紐付けられたケース、直接URLを本文に記載 |
.envのインラインコメントバグ
IRIS_EXTERNAL_URL=http://10.0.0.5 # production hostという記述が、http://10.0.0.5 # production hostという文字列全体として解析されていた。環境変数行のインラインコメントが、値を静かに破壊していた。
.envファイルを解析する際はインラインコメントを除去するか、Pythonのpython-dotenvのような適切なパーサーを使用すること。
シミュレーションと本番のデコーダー分岐
Windowsからの実際のWazuh agentイベントはwindows_eventchannelデコーダーチェーンを使用する。syslogで注入されたシミュレーションイベントはJSONデコーダーを使用する。この2つは完全に排他的――windows_eventchannelチェーンのルールはシミュレーションイベントに対して絶対にfireしない。
flowchart TD
A["Incoming Windows Event"]
B{"Source type?"}
C["Real Wazuh Agent"]
D["Syslog-injected Simulator"]
E["windows_eventchannel decoder"]
F["JSON decoder"]
G["Production anchor rule"]
H["Simulation anchor rule 100270"]
I["16 A4 Production rules"]
J["16 A4 Simulation rules"]
A --> B
B -- "Agent" --> C
B -- "Simulator" --> D
C --> E
D --> F
E --> G
F --> H
G --> I
H --> J
解決策:JSONデコーダーパス用のアンカールール(100270)を作成し、A4のシミュレーションルール16本すべてを本番アンカーではなくそこへ向ける。
JSON整形は検知パイプラインの一部
WindowsイベントのAlertの説明文が、1行の圧縮JSONとして表示されていた。2箇所の修正が必要だった。
- integratorサイド:JSON文字列を検出し、IRISへ保存する前に2スペースのインデントで整形する
- フロントエンドサイド:
<span>要素は空白を折りたたむ――<pre style="white-space:pre-wrap">へ変更することで整形済みJSONが正しく表示される
1行の圧縮JSONとして表示されたAlertの説明文は無視される。インデントで整形された同じデータは読まれる。表示形式は検知パイプラインの一部だ。
第5週:フィードバックループの完結とFalse Positiveの撲滅(3月31日〜4月1日)
Wazuhフィードバックループのクローズ
Cシリーズの検知(C1 impossible travel、C2 credential abuse、C3 lateral movement)は正しく動作していたが、無音だった。integratorはマッチを確認してIRIS Alertを作成する――しかしWazuh自身は何も知らなかった。
これが問題となる理由は2つある。
- Wazuhの
level 15ルールは、rawイベントではなく確認済みの攻撃に対してfireすべきだ - Wazuhが確認済みの検知を把握していなければ、SOCダッシュボードにも反映されない
解決策:WazuhSyslogAdapter――integrator内の小さなUDP送信機。C1マッチを確認後、integratorはWazuhへ構造化syslogイベントを返送する。
soc_event=correlation event_type=c1_impossible_travel user="..." src_ip=...
Wazuhはsoc-prod-integratorデコーダーを通じて解析し、アンカールール100260を経由してルール110502をlevel 15(確認済みcritical)でfireする。ループが閉じる。ダッシュボードが真実を映し出す。
flowchart TD
A["Login event from two locations"]
B["Wazuh raw rule fires (low level)"]
C["OpenSearch Indexer"]
D["soc-integrator poller"]
E["C1 correlation logic"]
F{"Impossible travel confirmed?"}
G["Discard"]
H["Create IRIS Alert"]
I["WazuhSyslogAdapter"]
J["UDP syslog back to Wazuh port 514"]
K["soc-prod-integrator decoder"]
L["Anchor rule 100260"]
M["Rule 110502 fires (level 15 critical)"]
N["SOC dashboards updated"]
O["Email notification sent"]
A --> B
B --> C
C --> D
D --> E
E --> F
F -- "No" --> G
F -- "Yes" --> H
H --> I
I --> J
J --> K
K --> L
L --> M
M --> N
H --> O
時間外検知のタイムゾーンエラー
C2のcredential abuse時間外ウィンドウは20:00–06:00 UTCで設定されていた。現地時間に直すまでは合理的に思えた。
20:00 UTC=03:00 ICT(バンコク)- バンコクの業務時間は
08:00 ICT=01:00 UTCから始まる
ルールが通常の朝の業務時間中にfireしていた。
修正後のウィンドウ:11:00–01:00 UTC = 18:00–08:00 ICT。時刻ベースの検知ルールは必ずアナリストのローカルタイムゾーンで設定し、UTCに換算すること。
C3-03:1日24回のFalse Positive → 0回
ルールC3-03はRDP type-3ログオンによる管理者のlateral movementを検知する。しかしFPBIADFS01で1日24回fireしていた。
根本原因:AD FSはsource IPを持たないサービス間type-3認証を常時実行する――ルールが探しているパターンと完全に一致するが、既知の安全なサービスアカウントからだ。
修正:条件を1つ追加する――ipAddressが存在し、かつloopbackでないこと。AD FSのサービス認証にはsource IPがないため、このガードでクリーンに除外できる。実際のリモートsourceを持つlateral movementは引き続きルールをトリガーする。
flowchart TD
A["Type-3 logon event detected"]
B{"ipAddress present and non-loopback?"}
C["FPBIADFS01 service auth (no IP)"]
D["Rule suppressed — false positive avoided"]
E["Remote admin session (real IP)"]
F["C3-03 fires — lateral movement alert"]
A --> B
B -- "No" --> C
C --> D
B -- "Yes" --> E
E --> F
条件1つ。False positive率:ゼロ。
commitメッセージには書かれなかったインフラの話
macOSのBind-Mount Inodeの問題
Docker on macOSはbind-mountされたファイルをinodeで追跡する。エディターがファイルを保存する際に新しいinodeを作成すると(sed -iや一部のIDEでよく起きる)、コンテナは古いinodeを読み続ける。症状:Wazuhルールを編集し、Wazuhをリロードし、テストしても――古い挙動が残る。
対処:macOSでbind-mountされた設定ファイルを編集した後は、必ずdocker compose up --force-recreateを実行する。初日からREADMEに記載しておくこと。
wazuh-logtestのstdinハング
docker exec -i経由で20行のテストファイルをwazuh-logtestにパイプすると、最後のイベントのphase 2以降で停止する。
対処:コンテナ内の一時ファイルに書き込んでからリダイレクトするか、タイムアウト付きでコマンドをラップする。
docker execを使った大容量ファイル転送
一部のホストではdocker exec経由のファイル転送が約64KBでパイプが切れる。
対処:ホスト側でファイルをbase64エンコードし、エンコード済み文字列をコンテナへパイプし、Python側でデコードする。
現状
2026年4月1日時点
| 対象領域 | 状態 |
|---|---|
| 付録A — Windows/ADシミュレーション | 59ルール、全件エンドツーエンドでfire確認済み |
| 付録B — ネットワーク/ファイアウォール | 本番ルール稼働中、FortiGate syslog受信中 |
| 付録C — アイデンティティ(C1/C2/C3) | 検知+Wazuhフィードバックループ完結 |
| Wazuh → IRIS sync | 稼働中、重大度・ルールIDでゲート制御 |
| IOCエンリッチメント | VT + AbuseIPDB + 4脅威フィード、CDBホットリロード |
| メール通知 | IRISの全webhookイベントに対して完全なAlert情報付きで送信 |
| False positive対策 | C3-03 ADFSガード、C2時間外タイムゾーン、ログオンタイプフィルター |
| ログ保持 | Wazuh 3日間、OpenSearch ISMポリシー30日間 |
まとめ:現場から得た教訓
1. 検知エンジニアリングは反復であり、積み上げではない
ルールを追加するcommitと、そのFalse positiveを修正するcommitは、同等に重要だ。何にでもfireするルールは、ルールがないよりも悪い――アナリストにアラートを無視する習慣をつけさせ、SOCの存在意義そのものを損なう。
2. フィードバックループが重要
IRISに届くがWazuhへ戻らない検知は、半分の検知だ。SIEMは相関エンジンが確認した内容を把握していなければならない。さもなければダッシュボードは嘘をつき、アナリストの信頼は失われる。
3. 小さなインフラのバグは積み重なる
タイムゾーンのエラー、.envのインラインコメントパースバグ、プライベートIPのVirusTotal送信――これらはいずれもデモを壊さない。しかし本番環境では定常的なノイズとなり、プラットフォームへの信頼を蝕む。「既知の問題」として放置される前に、早期に修正すること。
4. WazuhルールのORの罠は必ず確認する
単一ルール内の複数<match>タグは、特定のデコーダーチェーンコンテキストでORとして動作する可能性がある。ルールが想定より広くfireする場合は、明示的なANDロジックを持つ単一の<regex>に統合すること。
5. JSON整形は見た目の問題ではない
1行の圧縮JSONとして表示されたAlertの説明文は流し読みされ、見過ごされる。適切なインデントで整形された同じデータは読まれ、アクションにつながる。表示形式は検知パイプラインの一部だ。
関連資料
- Wazuh ルール構文ドキュメント
- IRIS-web (DFIR-IRIS) ドキュメント
- OpenSearch ISMポリシー
- Feodo Tracker · URLhaus · ThreatFox · MalwareBazaar
執筆:Simplico Co., Ltd. — バンコクを拠点とするソフトウェアエンジニアリング・プロダクトスタジオ。AI/RAGシステム、サイバーセキュリティ/SOCソリューション、エンタープライズシステムインテグレーションを専門とし、タイ・日本・中国・グローバル市場に対応。
スタック:Wazuh 4.x · IRIS-web · soc-integrator (FastAPI) · OpenSearch · Docker Compose · macOS開発環境
Get in Touch with us
Related Posts
- Payment APIにおけるIdempotencyとは何か
- Agentic AI × SOCワークフロー:プレイブックを超えた自律防御【2026年版ガイド】
- ECと基幹システムの二重入力をなくす:受注から仕訳までの自動化アーキテクチャ
- SIerのブラックボックスから脱却する:オープンソースで構築する中小企業向けSOCアーキテクチャ
- リサイクル工場管理システム:日本のリサイクル事業者が見えないところで損をしている理由
- エネルギー管理ソフトウェアのROI:電気代を15〜40%削減できる理由
- Wazuh + オープンソースで構築する軽量SOC:実践ガイド(2026年版)
- ECサイトとERPを正しく連携する方法:実践ガイド(2026年版)
- AI コーディングアシスタントが実際に使うツールとは?(Claude Code・Codex CLI・Aider)
- 燃費を本気で改善する:高負荷・低回転走行の物理学
- タイ産ドリアン・青果物デポ向け倉庫管理システム(WMS)— ERP連携・輸出書類自動化
- 現代のドリアン集荷場:手書き台帳をやめて、システムでビジネスを掌握する
- AI System Reverse Engineering:AIでレガシーソフトウェアシステムを理解する(Architecture・Code・Data)
- 人間の優位性:AIが代替できないソフトウェア開発サービス
- ゼロからOCPPへ:ホワイトラベルEV充電プラットフォームの構築
- Wazuh Decoders & Rules: 欠けていたメンタルモデル
- 製造現場向けリアルタイムOEE管理システムの構築
- 古い価格や在庫を表示しないECサイトのキャッシュ戦略
- AIによるレガシーシステム modernization:ERP・SCADA・オンプレミス環境へのAI/ML統合ガイド
- RAGアプリが本番環境で失敗する理由(そして解決策)













