VakulTech Logo
Back to Blog
Tutorial 6/20/2026

How to Build a Free Open-Source SOC

J
John Hedwig Trillana
5 min read • 246 views
How to Build a Free Open-Source SOC

Why Build Your Own SOC?

A Security Operations Center (SOC) used to mean expensive commercial licenses, dedicated hardware, and a team of analysts. That's no longer true. With Proxmox as your hypervisor and a handful of best-in-class open-source tools, a single IT administrator can stand up a fully functional SOC capable of monitoring servers, detecting network intrusions, visualizing system health, and pushing real-time alerts to email and Telegram — all for the cost of the hardware you already own.

This guide walks through the exact stack and commands used to build a working SOC for a small office/hospital ICT environment, end to end: • Proxmox VE — the hypervisor hosting everything • Wazuh — SIEM, endpoint detection, and compliance dashboards • Suricata — network intrusion detection (IDS) • Prometheus + Grafana — infrastructure metrics and dashboards • Email + Telegram — real-time alert notifications

Estimated time: 3–5 hours for a first-time build. Difficulty: Intermediate — comfort with Linux command line is helpful but every command is provided exactly as run.

Prerequisites

• A server or workstation with Proxmox VE already installed • At minimum: 8 CPU cores, 16 GB RAM, 100+ GB free storage (more is better — this guide used a host with 62 GB RAM and 5+ TB storage) • A network connection with internet access for downloading packages • A free Gmail account (for email alerts) and a Telegram account (for chat alerts)

Architecture Overview

Each major component runs as its own isolated guest on Proxmox — either a full VM or a lightweight LXC container — which keeps the stack easy to back up, snapshot, and troubleshoot independently.

image.png

Phase 1: Deploying Wazuh on Proxmox

Wazuh is the heart of the SOC — it's the SIEM that collects logs, correlates events into alerts, and gives you the dashboard you'll live in day to day. The fastest deployment path is Wazuh's official pre-built OVA, which bundles the manager, indexer, and dashboard into a single virtual appliance.

Step 1.1 — Download the Wazuh OVA

Download the latest Wazuh OVA from the official documentation page:

https://documentation.wazuh.com/current/deployment-options/virtual-machine/virtual-machine.html

At the time of this build, the latest version was Wazuh 4.14.5. Upload the .ova file to your Proxmox node's local storage (Datacenter → your node → local → Import).

Step 1.2 — Fix Storage Content Type (common error)

If you try to import the OVA through the GUI and get the error below, it's because your local storage isn't configured to accept disk images by default:

Parameter verification failed. (400) ide0: import working storage 'local' does not support 'images' content type or is not file based.

Fix it by enabling the images content type on local storage from the Proxmox shell:

pvesm set local --content iso,backup,images,rootdir

This is the single most common blocker when importing any OVA into Proxmox for the first time — worth remembering for future imports too.

Step 1.3 — Import the OVA via Command Line (most reliable)

The Proxmox GUI importer can be finicky depending on where the uploaded OVA actually lands on disk. The CLI method below is the most reliable path and is what was used in this build.

First, locate the uploaded OVA (Proxmox sometimes stores it under /var/lib/vz/import/ rather than the ISO folder):

find / -name "wazuh*.ova" 2>/dev/null

image.png

Locating the uploaded OVA under Datacenter → local → Import

Extract the OVA — it's just a tar archive containing the disk image:

cd /var/lib/vz/import/ tar xvf wazuh-4.14.5.ova ls -lh

image.png

Create the VM shell, then import the extracted .vmdk into it:

qm create 101 --name Wazuh-4.14.5 --memory 8192 --cores 4
--net0 virtio,bridge=vmbr0 --ostype l26

qm importdisk 101 wazuh-4.14.5-disk-1.vmdk local-lvm

Step 1.4 — Attach the Disk and Configure Boot

qm set 101 --scsi0 local-lvm:vm-101-disk-0 qm set 101 --ide2 none,media=cdrom qm set 101 --boot order=scsi0 qm set 101 --serial0 socket --vga serial0

Step 1.5 — Fix CPU Compatibility (common error)

On first boot you may hit a CPU compatibility error if your physical CPU doesn't support the instruction set the OVA was built for:

Fatal glibc error: CPU does not support x86-64-v2

Fix it by passing your host CPU through directly instead of emulating a generic profile:

qm stop 101 qm set 101 --cpu host qm start 101

Step 1.6 — Boot and First Login

qm start 101

Open the Console tab for the VM in Proxmox. Wazuh's Amazon Linux base takes 1–2 minutes to finish booting on first start — press Enter once it settles to see the login banner.

image.png

Find the VM's IP address from inside the console:

ip a

Then open a browser to the IP shown (e.g. https://10.5.49.164) and log in with the default credentials, changing the password immediately on first login:

Username: admin Password: admin

Step 1.7 — Set a Static IP (so it survives reboots)

By default the OVA uses DHCP. Edit the network config to lock in a permanent address:

sudo nano /etc/sysconfig/network-scripts/ifcfg-eth0

DEVICE=eth0 BOOTPROTO=none ONBOOT=yes TYPE=Ethernet IPADDR=10.5.49.164 PREFIX=22 GATEWAY=10.5.48.1 DNS1=8.8.8.8 DNS2=1.1.1.1

Apply and verify:

sudo systemctl restart network ip a | grep inet

Reboot the VM once after this change and confirm the dashboard still loads at the same IP — that confirms the static IP survives a restart.

Step 1.8 — Deploy Your First Agent

From the Wazuh dashboard: Endpoints → Deploy new agent. Choose the OS of the machine you want to monitor, enter the Wazuh server address, give the agent a name, and the dashboard generates the exact install command to run on that target machine.

image.png Wazuh Overview dashboard with an active agent reporting alerts

Phase 1 complete: Wazuh SIEM is live, accessible on a static IP, and receiving data from at least one endpoint agent.

Phase 2: Deploying Suricata (Network IDS)

Suricata watches raw network traffic and flags known attack signatures, port scans, and suspicious protocol behavior. Deploying it as a lightweight LXC container keeps resource usage minimal.

Step 2.1 — Create the Suricata LXC Container

pveam update pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.zst

If you see “storage 'local' does not support templates”, run: pvesm set local --content iso,backup,images,rootdir,vztmpl

pct create 102 local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst
--hostname suricata --memory 2048 --cores 2
--rootfs local-lvm:20 --net0 name=eth0,bridge=vmbr0,ip=dhcp
--password suricata123 --unprivileged 1 --start 1

Step 2.2 — Enter the Container and Fix DNS

LXC containers on a network with a custom gateway sometimes can't resolve external DNS by default. Point resolv.conf at your gateway and a public resolver:

pct enter 102 printf "nameserver 10.5.48.1\nnameserver 8.8.8.8\n" > /etc/resolv.conf ping -c 2 google.com

If your network has firewall/hotspot restrictions that block container IPs from reaching the internet directly, whitelist the container's IP (shown by 'ip a') on your gateway/hotspot, or fetch packages on the Proxmox host and push them into the container with 'pct push' instead.

Step 2.3 — Install Suricata

apt update && apt upgrade -y apt install software-properties-common -y add-apt-repository ppa:oisf/suricata-stable -y apt install suricata -y suricata-update

Step 2.4 — Configure and Start Suricata

sed -i 's/community-id: false/community-id: true/' /etc/suricata/suricata.yaml systemctl restart suricata systemctl status suricata --no-pager

image.png Suricata service active and running with 374 rules loaded

Step 2.5 — Install the Wazuh Agent on the Suricata Container

This is what ships Suricata's network alerts into the Wazuh dashboard for correlation alongside your endpoint logs.

apt install curl -y curl -o /tmp/wazuh.key https://packages.wazuh.com/key/GPG-KEY-WAZUH gpg --no-default-keyring --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg
--import /tmp/wazuh.key chmod 644 /usr/share/keyrings/wazuh.gpg

echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg]
https://packages.wazuh.com/4.x/apt/ stable main" |
tee /etc/apt/sources.list.d/wazuh.list

apt update && WAZUH_MANAGER='10.5.49.164' apt install wazuh-agent -y systemctl enable wazuh-agent && systemctl start wazuh-agent

Tell the agent to forward Suricata's eve.json log to Wazuh:

cat >> /var/ossec/etc/ossec.conf << 'EOF' <ossec_config> <log_format>json</log_format> /var/log/suricata/eve.json </ossec_config> EOF

systemctl restart wazuh-agent

image.png Wazuh agent active on the Suricata container, shipping network alerts

Phase 2 complete: Suricata is actively inspecting traffic and forwarding alerts into Wazuh as a second monitored agent.

Phase 3: Prometheus + Grafana (Metrics & Dashboards)

While Wazuh handles security events, Prometheus and Grafana give you visibility into system health — CPU, memory, disk, and network trends over time. This guide deploys both inside a single LXC container.

Step 3.1 — Create the Monitoring Container

pct create 103 local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst
--hostname monitoring --memory 2048 --cores 2
--rootfs local-lvm:20 --net0 name=eth0,bridge=vmbr0,ip=dhcp
--password monitor123 --unprivileged 1 --start 1

pct enter 103 printf "nameserver 10.5.48.1\nnameserver 8.8.8.8\n" > /etc/resolv.conf apt update && apt upgrade -y

Step 3.2 — Download Binaries (host-side fallback)

If your network blocks direct container internet access, download on the Proxmox host (which already has internet) and push the files into the container — this completely bypasses any container-level network restriction:

On the Proxmox host:

cd /tmp wget https://github.com/prometheus/prometheus/releases/download/v2.51.2/prometheus-2.51.2.linux-amd64.tar.gz wget https://dl.grafana.com/oss/release/grafana_10.4.2_amd64.deb wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz

pct push 103 /tmp/prometheus-2.51.2.linux-amd64.tar.gz /tmp/prometheus.tar.gz pct push 103 /tmp/grafana_10.4.2_amd64.deb /tmp/grafana.deb pct push 103 /tmp/node_exporter-1.7.0.linux-amd64.tar.gz /tmp/node_exporter.tar.gz

Step 3.3 — Install Everything Inside the Container

pct enter 103 cd /tmp

Prometheus

tar xzf prometheus.tar.gz mv prometheus-2.51.2.linux-amd64/prometheus /usr/local/bin/ mv prometheus-2.51.2.linux-amd64/promtool /usr/local/bin/ mkdir -p /etc/prometheus /var/lib/prometheus mv prometheus-2.51.2.linux-amd64/prometheus.yml /etc/prometheus/

Node Exporter

tar xzf node_exporter.tar.gz mv node_exporter-1.7.0.linux-amd64/node_exporter /usr/local/bin/

Grafana

apt install -y adduser libfontconfig1 musl dpkg -i /tmp/grafana.deb

Step 3.4 — Create systemd Services and Configure Scraping

cat > /etc/systemd/system/prometheus.service << 'EOF' [Unit] Description=Prometheus After=network.target

[Service] ExecStart=/usr/local/bin/prometheus
--config.file=/etc/prometheus/prometheus.yml
--storage.tsdb.path=/var/lib/prometheus Restart=always

[Install] WantedBy=multi-user.target EOF

cat > /etc/systemd/system/node_exporter.service << 'EOF' [Unit] Description=Node Exporter After=network.target

[Service] ExecStart=/usr/local/bin/node_exporter Restart=always

[Install] WantedBy=multi-user.target EOF

Tell Prometheus to actually scrape node_exporter — this step is easy to miss and leaves Grafana showing “No data”:

cat > /etc/prometheus/prometheus.yml << 'EOF' global: scrape_interval: 15s

scrape_configs:

  • job_name: 'prometheus' static_configs:

    • targets: ['localhost:9090']
  • job_name: 'node_exporter' static_configs:

    • targets: ['localhost:9100'] EOF

systemctl daemon-reload systemctl enable --now prometheus node_exporter grafana-server

Step 3.5 — Configure Grafana

Open http://:3000 and log in with the default admin/admin credentials (you'll be prompted to change it). Then:

  1. Connections → Data sources → Add data source → Prometheus, URL: http://localhost:9090, click Save & test
  2. Dashboards → New → Import → enter dashboard ID 1860 (Node Exporter Full) → Load → Import

image.png Grafana home screen after first login

Once the Job and Instance filters are set to node_exporter, live metrics populate immediately:

image.png Grafana dashboard showing live CPU, memory, disk, and network metrics

Phase 3 complete: Prometheus is scraping metrics and Grafana is rendering live infrastructure dashboards.

**Phase 4: Real-Time Alerts — Email and Telegram**

A SOC that no one checks isn't a SOC — it's a log archive. The final piece is pushing high-severity alerts directly to your inbox and phone the moment they happen.

Step 4.1 — Configure Wazuh Email Alerts

Edit the global block in Wazuh's main config file:

sudo nano /var/ossec/etc/ossec.conf <jsonout_output>yes</jsonout_output> <alerts_log>yes</alerts_log> no <logall_json>no</logall_json> <email_notification>yes</email_notification> <smtp_server>localhost</smtp_server> <email_from>yourname@gmail.com</email_from> <email_to>yourname@gmail.com</email_to> <email_to>second.recipient@gmail.com</email_to> <email_maxperhour>12</email_maxperhour> <email_log_source>alerts.log</email_log_source> <agents_disconnection_time>15m</agents_disconnection_time> <agents_disconnection_alert_time>0</agents_disconnection_alert_time> <update_check>yes</update_check>

3 10

Multiple recipients are supported by simply repeating the <email_to> tag — there's no limit beyond your maxperhour throughput.

Step 4.2 — Get a Gmail App Password

Gmail blocks direct password login from scripts, so a 16-character App Password is required: 3. Enable 2-Step Verification on the Gmail account (myaccount.google.com → Security) 4. Security → App passwords → name it “Wazuh” → Create 5. Copy the 16-character password shown — it's only displayed once

Step 4.3 — Install and Configure Postfix as the SMTP Relay

sudo yum install postfix cyrus-sasl-plain mailx -y

sudo tee -a /etc/postfix/main.cf << 'EOF' relayhost = [smtp.gmail.com]:587 smtp_sasl_auth_enable = yes smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd smtp_sasl_security_options = noanonymous smtp_tls_security_level = encrypt smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt EOF

sudo bash -c 'echo "[smtp.gmail.com]:587 yourname@gmail.com:yourapppassword" \

/etc/postfix/sasl_passwd'

sudo chmod 600 /etc/postfix/sasl_passwd sudo postmap /etc/postfix/sasl_passwd sudo systemctl enable --now postfix

Test it:

echo "Test from Wazuh SOC" | mail -s "Wazuh Test" yourname@gmail.com

sudo systemctl restart wazuh-manager

image.png Test email successfully delivered via the Postfix → Gmail relay

Step 4.4 — Set Up a Telegram Bot (faster than Viber, no business approval needed)

  1. In Telegram, message @BotFather and send /newbot
  2. Give it a name and a username ending in _bot — BotFather returns a token
  3. Message your new bot (click Start, send any text)
  4. Open https://api.telegram.org/bot/getUpdates in a browser and copy the chat.id value — that's your Chat ID

Step 4.5 — Build the Telegram Integration Script

sudo cat > /var/ossec/integrations/custom-telegram << 'EOF' #!/usr/bin/env python3 import sys, json, urllib.request

TELEGRAM_TOKEN = "YOUR_BOT_TOKEN" CHAT_ID = "YOUR_CHAT_ID"

def send_telegram(message): url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage" data = json.dumps({ "chat_id": CHAT_ID, "text": message, "parse_mode": "HTML" }).encode("utf-8") req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}) urllib.request.urlopen(req)

alert_file = open(sys.argv[1]) alert = json.load(alert_file) alert_file.close()

level = alert["rule"]["level"] desc = alert["rule"]["description"] agent = alert.get("agent", {}).get("name", "Unknown") msg = f"\U0001F6A8 Wazuh SOC Alert\nLevel: {level}\n"
f"Agent: {agent}\nRule: {desc}" send_telegram(msg) EOF

sudo chmod 750 /var/ossec/integrations/custom-telegram sudo chown root:wazuh /var/ossec/integrations/custom-telegram

Register the integration in ossec.conf, just before the closing </ossec_config> tag:

custom-telegram 10 json

sudo systemctl restart wazuh-manager

Step 4.6 — Test It

Trigger a real brute-force alert by attempting several failed SSH logins:

for i in {1..10}; do ssh invalid@localhost; done

press Enter through each failed password prompt

image.png A live Wazuh brute-force alert arriving in Telegram

Phase 4 complete: high-severity Wazuh alerts now land in both email and Telegram in real time.

Common Errors and Fixes image.png

What's Next: Extending the SOC

With the core four phases running, a few additions meaningfully raise the maturity of a one-person SOC: • TheHive — proper incident case tracking instead of relying on memory • MISP — threat intelligence feeds to auto-tag known-bad IPs and file hashes • OpenVAS/Greenbone — scheduled vulnerability scanning across all assets • Cloud log forwarding — pipe AWS CloudTrail / GCP audit logs into Wazuh for hybrid visibility • Wazuh Active Response — auto-block IPs on confirmed brute-force attempts

Final Result

By the end of this build: Wazuh is correlating endpoint and network events from multiple agents, Suricata is inspecting all network traffic for known attack signatures, Grafana is visualizing live system health, and every high-severity event reaches you by email and Telegram within seconds — all running on infrastructure you already own, using entirely free and open-source software. If you run into an error not covered in the troubleshooting table above, the most common culprits are storage content-type settings, DNS inside LXC containers, and disk bus mismatches — check those three first.

To download a PDF copy of the article, click the button below:

Premium Content Blocked

The following content is reserved for users with the FREE plan or higher. Please log in or upgrade your account to continue reading.

Log In or Register
0 Comments

Discussion

You must be logged in to participate in the discussion.

Log In
No comments yet. Be the first to share your thoughts!