← Back

Building an Open-Source SOC: The Full Stack

About This Guide

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.

Complete SOC Platform Architecture Diagram
Figure 1: Complete SOC Platform Architecture, 7 open-source components working as one system

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 Specifications
ServervCPURAMDiskRole
soc-node-0116 vCPU64 GB1 TB SSDOpenSearch + Graylog
soc-node-0216 vCPU64 GB1 TB SSDOpenSearch + Graylog
soc-node-038 vCPU32 GB500 GBWazuh Manager
soc-node-048 vCPU16 GB200 GBGrafana + Shuffle
soc-node-054 vCPU16 GB200 GBIRIS + MISP + OpenCTI
Total52 vCPU192 GB2.9 TBAll components

OS: Ubuntu 22.04 LTS on all nodes

Hardware Note

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.

Shell, Run on every server
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
Terminal showing OS preparation complete on soc-node-01
Figure 2: OS preparation complete, all 5 nodes ready for component installation

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.

Shell, Install OpenSearch on node-01 and node-02
# 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 }
OpenSearch cluster health showing green status with 2 nodes
Figure 3: OpenSearch cluster healthy, 2 nodes, green status, ready for data ingestion

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.

Shell, Install Wazuh Manager
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
Wazuh manager dashboard showing connected agents
Figure 4: Wazuh Manager running, Filebeat forwarding alerts to OpenSearch
Multi-Node Note

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

Shell, Deploy Wazuh Agent on Linux
# 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
PowerShell, Deploy Wazuh Agent on Windows
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)

Shell, Install Graylog
# 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.

Graylog, Syslog Input Configuration
System → Inputs → Launch new input:
  Type: Syslog UDP
  Port: 514
  Bind address: 0.0.0.0
  Allow override date: Yes
Graylog dashboard showing firewall syslog streams with parsed fields
Figure 5: Graylog receiving firewall syslog, 60-70 GB/day, parsed with extractors

Step 6: Configure Firewall Syslog Forwarding

Palo Alto, Syslog Server Profile
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.

Shell, Install Grafana
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
Grafana, Add Datasources via UI
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.

Grafana dashboard showing combined Wazuh alerts and Graylog network flows
Figure 6: Grafana dashboard, Wazuh alerts panel (left) + Graylog firewall traffic (right) on single pane

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.

Shell, Install Shuffle
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

Shuffle Workflow Nodes (in order)
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 }}
Shuffle workflow showing all 7 nodes connected
Figure 7: Shuffle SOAR workflow, 7 nodes, fully automated from alert to case creation

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, Get Webhook URI
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>
XML, /var/ossec/etc/ossec.conf (on soc-node-03)
<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
Built-in vs Custom

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+.

Wazuh ossec.log showing integration executed successfully
Figure 8: Wazuh log confirming built-in Shuffle integration forwarding alerts

Step 10: IRIS, DFIR Case Management (soc-node-05)

Shell, Install IRIS
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:

MISP, GUI Feed Configuration
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
MISP dashboard showing imported threat feeds and IoC correlations
Figure 9: MISP with imported threat feeds, feeding IoC data to Shuffle enrichment

Step 12: End-to-End Test

Test: Trigger an alert and verify the full pipeline
# 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
Terminal showing successful end-to-end test with all checkpoints passed
Figure 10: End-to-end test results, all 5 checkpoints passed successfully

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.

Comparison
Before (Commercial)After (Open-Source)
Annual Cost~10B IDR (~$600K)Electricity only
Ingestion Cap100 GB/dayUnlimited (HW bound)
CorrelationPartial (add-on)Full (Grafana dual)
Automation (SOAR)Not includedFull (Shuffle)
Block TimeManual (hours)Automated (<10 sec)
Case ManagementSeparate licenseIncluded (IRIS)
Threat IntelExtra costIncluded (MISP/CTI)
Data Loss30-40% daily0%
Deployment Notes

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