Inspired by Bayu Sangkaya's real deployment that replaced a 10B IDR/year commercial SIEM with open-source. I wasn't the one who deployed it — this is my take on the architecture, built from the stack I know. Commands and configs verified against official docs (mid-2026). Test in a lab before production.
A government institution was paying 10 billion IDR per year (~$600K USD) for a commercial SIEM license. The license capped ingestion at 100 GB/day. Their firewall alone pushed 60-70 GB/day. WAF and servers added 40 GB more. By 2 PM every day, the cap was hit and events were silently dropped, no warning, no alert, just missing data. The solution: replace the entire stack with open-source tools. Zero subscription cost. Full correlation. Automated blocking in under 10 seconds. Here's exactly how, step by step.
Pre-requisites & Server Sizing
These specs are based on a real deployment documented by Bayu Sangkaya. The original setup handled 100 GB/day ingestion comfortably. The split is deliberate: OpenSearch gets two nodes because it's the choke point — every byte flows through it. Wazuh gets its own node because the manager's analysis engine is CPU-intensive during peak hours. Grafana and Shuffle share a node because they're lightweight compared to the indexer. IRIS and MISP share the smallest node — threat intel and case management are low-throughput, high-availability services.
If you're on a budget, you can collapse this into 1-2 nodes for a lab. But in production, don't put OpenSearch on the same machine as anything else — the JVM heap contention will kill performance.
| Server | vCPU | RAM | Disk | Role |
|---|---|---|---|---|
| soc-node-01 | 16 vCPU | 64 GB | 1 TB SSD | OpenSearch + Graylog |
| soc-node-02 | 16 vCPU | 64 GB | 1 TB SSD | OpenSearch + Graylog |
| soc-node-03 | 8 vCPU | 32 GB | 500 GB | Wazuh Manager |
| soc-node-04 | 8 vCPU | 16 GB | 200 GB | Grafana + Shuffle |
| soc-node-05 | 4 vCPU | 16 GB | 200 GB | IRIS + MISP + OpenCTI |
| Total | 52 vCPU | 192 GB | 2.9 TB | All components |
OS: Ubuntu 22.04 LTS on all nodes
This institution already owned the hardware. The only cost was electricity and maintenance. If you're starting fresh, equivalent cloud instances (AWS/GCP/Azure) would run $2,000-3,000/month, still significantly cheaper than the 10B IDR/year commercial license they were paying.
Step 1: OS Preparation (All Nodes)
Run this on every server before installing anything. Two things bite people here: swap and vm.max_map_count. OpenSearch's JVM will crash if swap is enabled — the heap needs locked memory. And the virtual memory map count default (65530) isn't enough for OpenSearch's index operations. Set it to 262144 first, save yourself a 2 AM debug session.
sudo apt update && sudo apt upgrade -y
sudo hostnamectl set-hostname <node-name>
# Disable swap (required for OpenSearch JVM)
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
# Increase virtual memory map count (OpenSearch requirement)
echo 'vm.max_map_count=262144' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
# Enable firewall ports
sudo ufw allow 22/tcp
sudo ufw allow 9200/tcp # OpenSearch
sudo ufw allow 1514:1516/tcp # Wazuh agents
sudo ufw allow 514/udp # Syslog (Graylog)
sudo ufw allow 443/tcp # Dashboard
sudo ufw --force enable
# Set timezone
sudo timedatectl set-timezone Asia/Jakarta
# Install dependencies
sudo apt install -y curl wget gnupg apt-transport-https jq
Step 2: OpenSearch Cluster (soc-node-01 & soc-node-02)
OpenSearch is the shared backend. Both Wazuh and Graylog write here. Grafana reads from here. No built-in GUI for OpenSearch itself — verify via API.
# Add OpenSearch repository
curl -o- https://artifacts.opensearch.org/publickeys/opensearch.pgp | sudo apt-key add -
echo "deb https://artifacts.opensearch.org/releases/bundle/opensearch/2.x/apt stable main" | sudo tee /etc/apt/sources.list.d/opensearch.list
sudo apt update && sudo apt install -y opensearch
# Configure node-01
sudo tee /etc/opensearch/opensearch.yml << 'EOF'
cluster.name: soc-cluster
node.name: soc-node-01
path.data: /var/lib/opensearch
path.logs: /var/log/opensearch
network.host: 0.0.0.0
http.port: 9200
discovery.seed_hosts: ["soc-node-01", "soc-node-02"]
cluster.initial_master_nodes: ["soc-node-01", "soc-node-02"]
plugins.security.disabled: true
EOF
# Set JVM heap (50% of RAM, max 32GB)
sudo tee -a /etc/opensearch/jvm.options << 'EOF'
-Xms16g
-Xmx16g
EOF
sudo systemctl enable --now opensearch
# Verify cluster health (API, no GUI for OpenSearch itself)
curl http://localhost:9200/_cluster/health
# Expected: { "status": "green", "number_of_nodes": 2 }
Step 3: Wazuh Manager (soc-node-03)
The Wazuh manager is the brain. It receives events from agents, runs decoders to parse raw logs, fires rules to detect threats, and forwards high-severity alerts to Shuffle. The all-in-one installer gets you a working manager in minutes. Filebeat ships the alerts to OpenSearch for indexing and dashboarding.
Why Filebeat? Wazuh could write directly to OpenSearch, but Filebeat handles buffering, retry logic, and backpressure — critical when you're processing 100 GB/day. If OpenSearch goes down for maintenance, Filebeat holds the queue.
curl -s https://packages.wazuh.com/4.x/wazuh-install.sh | sudo bash
# Configure Filebeat to send alerts to OpenSearch
sudo tee /etc/filebeat/filebeat.yml << 'EOF'
output.elasticsearch:
hosts: ["https://soc-node-01:9200", "https://soc-node-02:9200"]
username: "admin"
password: "admin"
ssl.verification_mode: none
index: "wazuh-alerts-%{+yyyy.MM.dd}"
setup.template:
enabled: false
EOF
sudo systemctl enable --now filebeat
sudo systemctl restart wazuh-manager
The all-in-one installer used here is for quick deployment. In a multi-node setup, install Wazuh server separately: wazuh-install.sh --wazuh-server wazuh-1. Skip the indexer and dashboard components since you already have OpenSearch on nodes 01-02 and Grafana on node-04.
Step 4: Wazuh Agents, Deploy to Windows & Linux Endpoints
# On each Linux server
curl -s https://packages.wazuh.com/4.x/wazuh-install.sh | sudo bash
# Register with manager
sudo /var/ossec/bin/agent-auth -m soc-node-03 -p 1515
sudo systemctl enable --now wazuh-agent
Invoke-WebRequest -Uri https://packages.wazuh.com/4.x/windows/wazuh-agent-4.7.0-1.msi -OutFile wazuh-agent.msi
Start-Process msiexec -ArgumentList "/i wazuh-agent.msi /qn WAZUH_MANAGER=soc-node-03 WAZUH_REGISTRATION_SERVER=soc-node-03" -Wait
Start-Service WazuhSvc
Step 5: Graylog + OpenSearch, Syslog Ingestion (soc-node-01)
# Install MongoDB (required by Graylog)
wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt update && sudo apt install -y mongodb-org
sudo systemctl enable --now mongod
# Install Graylog
wget https://packages.graylog2.org/repo/packages/graylog-5.2-repository_latest.deb
sudo dpkg -i graylog-5.2-repository_latest.deb
sudo apt update && sudo apt install -y graylog-server
# Configure Graylog to use existing OpenSearch
sudo tee -a /etc/graylog/server/server.conf << 'EOF'
is_leader = true
node_id_file = /etc/graylog/server/node-id
password_secret = CHANGE_THIS_SECRET_KEY_256_BIT
root_password_sha2 = SHA256_OF_YOUR_PASSWORD
http_bind_address = 0.0.0.0:9000
elasticsearch_hosts = https://soc-node-01:9200,https://soc-node-02:9200
EOF
sudo systemctl enable --now graylog-server
Graylog web interface at http://soc-node-01:9000. Login with admin / password you set in root_password_sha2. From the GUI: System → Inputs to configure syslog listeners. Create Streams to route firewall traffic. Add Extractors to parse fields from raw syslog messages.
System → Inputs → Launch new input:
Type: Syslog UDP
Port: 514
Bind address: 0.0.0.0
Allow override date: Yes
Step 6: Configure Firewall Syslog Forwarding
Device → Server Profiles → Syslog → Add:
Name: graylog-syslog
Server: soc-node-01
Transport: UDP
Port: 514
Format: BSD
Facility: LOG_USER
Policies → Security → Log Settings:
☑ Log at Session Start
☑ Log at Session End
☑ Send to syslog: graylog-syslog
Step 7: Grafana, Unified Correlation Dashboard (soc-node-04)
Wazuh has its own dashboard. Graylog has its own. You don't want two tabs open during an investigation. Grafana pulls from both via the OpenSearch datasource plugin and gives you one pane. Alert severity on the left. Firewall traffic on the right. Same timestamp. Same refresh rate. One decision. This is where the SIEM stops being a log viewer and starts being an investigation tool.
sudo apt-get install -y software-properties-common
sudo add-apt-repository "deb https://packages.grafana.com/oss/deb stable main"
wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
sudo apt update && sudo apt install -y grafana
sudo systemctl enable --now grafana-server
# Install OpenSearch datasource plugin (Grafana 10.4+)
sudo grafana-cli plugins install grafana-opensearch-datasource
Configuration → Data Sources → Add data source
Datasource 1: Wazuh-Alerts
Type: OpenSearch
URL: https://soc-node-01:9200
Index pattern: wazuh-alerts-*
Time field: timestamp
Version: 2.x+
Datasource 2: Graylog-Network
Type: OpenSearch
URL: https://soc-node-01:9200
Index pattern: graylog_*
Time field: timestamp
After adding datasources, import dashboards:
Dashboards → Import → Upload JSON or paste panel config
Build a unified view: Wazuh alerts panel (left) + Graylog firewall traffic (right)
Grafana web at http://soc-node-04:3000. Default login admin/admin. After OpenSearch datasource is added, go to Explore → select OpenSearch → run queries like rule.level: >= 12 to confirm data is flowing.
Step 8: Shuffle SOAR, Automation Engine (soc-node-04)
Detection without response is just observation. Wazuh sees the threat. Shuffle does something about it. At scale, you can't manually block IPs for every Wazuh alert. The math doesn't work: 50 alerts/hour × 5 minutes per response = an SOC analyst doing nothing but blocks. This pipeline automates the repetitive stuff — enrichment, IP blocking, case creation, Slack notification — so your analysts focus on the novel attacks. The ones automation hasn't seen before.
docker run -d --name shuffle \
-p 3001:3001 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v shuffle-data:/data \
ghcr.io/shuffle/shuffle:latest
Shuffle Workflow: Alert → Enrich → Block → Case → Notify
1. Webhook (Trigger)
URI: /api/v1/hooks/webhook_YOUR_UUID
Receives: JSON alert from Wazuh integration
Click Start to begin listening
2. Set (Normalize Fields)
srcip: {{ $json.data.srcip }}
rule_id: {{ $json.rule.id }}
rule_level: {{ $json.rule.level }}
description: {{ $json.rule.description }}
3. HTTP Request → MISP (Enrichment)
Method: POST
URL: https://soc-node-05/attributes/restSearch
Body: {"value":"{{ srcip }}","type":"ip-src"}
Header: Authorization: MISP_API_KEY
4. Condition (IF enrichment found)
{{ $json.response.Attribute | length }} > 0
5. IF TRUE → HTTP Request → Firewall (Block IP)
Method: POST
URL: https://firewall.local/api/v2/cmdb/firewall/addrgrp/Blocked-IPs/member
Body: {"name":"auto-block-{{ srcip }}","subnet":"{{ srcip }}/32"}
Header: Authorization: Bearer FORTIGATE_API_TOKEN
6. THEN → HTTP Request → IRIS (Create Case)
Method: POST
URL: https://soc-node-05/api/v2/cases
Body: {"case_title":"{{ description }}","case_description":"IP: {{ srcip }}","case_severity":3}
Header: Authorization: Bearer IRIS_API_KEY
7. FINALLY → Slack (Notify)
Channel: #soc-alerts
Text: 🚨 Auto-blocked {{ srcip }} | {{ description }} | Case: #{{ case_id }}
Step 9: Register Wazuh → Shuffle Integration
Wazuh 4.4+ includes built-in Shuffle integration. No custom scripts needed. In Shuffle, create a webhook trigger and copy its URI. Then add the integration block to Wazuh.
Shuffle → Workflows → + Create Workflow → Name it "Wazuh SOC Pipeline"
Drag Webhook trigger to canvas → Click it → Copy webhook URI
URI format: https://soc-node-04:3001/api/v1/hooks/webhook_<UUID>
<ossec_config>
<integration>
<name>shuffle</name>
<hook_url>https://soc-node-04:3001/api/v1/hooks/webhook_YOUR_UUID</hook_url>
<level>12</level>
<alert_format>json</alert_format>
</integration>
</ossec_config>
# Built-in since Wazuh 4.4+. No Python/Bash script required.
# Restart manager after saving:
sudo systemctl restart wazuh-manager
Wazuh 4.4+ ships Shuffle as a native integration (<name>shuffle</name>). Older versions require a custom script at /var/ossec/integrations/custom-shuffle. This guide assumes Wazuh 4.7+.
Step 10: IRIS, DFIR Case Management (soc-node-05)
git clone https://github.com/dfir-iris/iris-web.git
cd iris-web
docker compose up -d
# Access: https://soc-node-05:8443
# Default: administrator / iris1234
# CHANGE IMMEDIATELY
Step 11: MISP, Threat Intelligence (soc-node-05)
MISP web at https://soc-node-05. Default login admin@admin.test/admin — change immediately. After install, add threat feeds from the GUI:
Sync Actions → Feeds → Add Feed:
- Abuse.ch SSL Blacklist (https://sslbl.abuse.ch/blacklist/)
- URLhaus (https://urlhaus.abuse.ch/downloads/csv_recent/)
- AlienVault OTX (https://otx.alienvault.com/)
Administration → Event Actions → Import Feeds (fetch & correlate)
Enable feed auto-fetch: Schedule → Check daily pull
Step 12: End-to-End Test
# 1. Simulate an attack from a known malicious IP
# Use a test IP from MISP's database
curl -H "X-Forwarded-For: 185.220.101.34" https://web-server.local/admin
# 2. Verify Wazuh logs the event
tail -f /var/ossec/logs/alerts/alerts.json | grep 185.220.101.34
# 3. Verify Shuffle receives the webhook
docker logs shuffle --tail 20
# 4. Verify IRIS case was created
curl -H "Authorization: Bearer IRIS_API_KEY" \
https://soc-node-05/api/v2/cases | jq '.data[].case_title'
# 5. Verify firewall block rule exists
# Check FortiGate/Palo Alto for new address object
The Result
After step 12, you have something that didn't exist before: a unified SOC platform built from open-source components that talks to itself. Wazuh detects. Shuffle responds. IRIS documents. MISP enriches. Grafana visualizes. You replaced a 10B IDR/year commercial SIEM that dropped 30-40% of your data. That's the headline. But the real win isn't the money. It's that your SOC team now sees everything. No silent data loss. No "we missed it because the license capped ingestion." Full visibility, zero subscription.
| Before (Commercial) | After (Open-Source) | |
|---|---|---|
| Annual Cost | ~10B IDR (~$600K) | Electricity only |
| Ingestion Cap | 100 GB/day | Unlimited (HW bound) |
| Correlation | Partial (add-on) | Full (Grafana dual) |
| Automation (SOAR) | Not included | Full (Shuffle) |
| Block Time | Manual (hours) | Automated (<10 sec) |
| Case Management | Separate license | Included (IRIS) |
| Threat Intel | Extra cost | Included (MISP/CTI) |
| Data Loss | 30-40% daily | 0% |
Everything was deployed remotely, outside office hours. SSH access was the only requirement. The institution's IT team handled physical rack mounting. I handled everything from OS installation to final testing. Remote work eliminates travel costs and scheduling delays, critical for government institutions outside major cities.
References
Wazuh Documentation. documentation.wazuh.com
OpenSearch, Cluster Formation. opensearch.org/docs
Graylog, Server Installation. go2docs.graylog.org
Grafana, OpenSearch Datasource. grafana.com/docs
Shuffle SOAR. shuffler.io/docs
DFIR-IRIS. docs.dfir-iris.org
MISP Project. misp-project.org
Wazuh. Integration with Shuffle. wazuh.com/blog
MISP Project. Wazuh Integration. misp-project.org
DFIR-IRIS. IrisMISP Module. github.com/dfir-iris
Fares Bltagy. Build Home Lab SOC Automation. faresbltagy.gitbook.io
Misje. OpenCTI-Wazuh Connector. misje.github.io
Palo Alto, Syslog Forwarding. docs.paloaltonetworks.com