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日、progressprogress-updaterule 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の不正利用履歴

バックグラウンドフィード取込:

WazuhのCDBリストファイル(malicious-ipmalicious-domainsmalware-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.x10.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箇所の修正が必要だった。

  1. integratorサイド:JSON文字列を検出し、IRISへ保存する前に2スペースのインデントで整形する
  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つある。

  1. Wazuhのlevel 15ルールは、rawイベントではなく確認済みの攻撃に対してfireすべきだ
  2. 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の説明文は流し読みされ、見過ごされる。適切なインデントで整形された同じデータは読まれ、アクションにつながる。表示形式は検知パイプラインの一部だ。


関連資料


執筆:Simplico Co., Ltd. — バンコクを拠点とするソフトウェアエンジニアリング・プロダクトスタジオ。AI/RAGシステム、サイバーセキュリティ/SOCソリューション、エンタープライズシステムインテグレーションを専門とし、タイ・日本・中国・グローバル市場に対応。

スタック:Wazuh 4.x · IRIS-web · soc-integrator (FastAPI) · OpenSearch · Docker Compose · macOS開発環境


Get in Touch with us

Chat with Us on LINE

iiitum1984

Speak to Us or Whatsapp

(+66) 83001 0222

Related Posts

Our Products