One command on a Proxmox host builds a complete, self-hosted Security Operations Center lab in about 30 minutes.
Website · What it does · Install
SOC Stack is a one-command installer that stands up a full open-source Security Operations Center on a single Proxmox host: Wazuh, TheHive + Cortex, MISP, Zeek + Suricata, dashboards, and a row of MCP servers, all wired together. You want a realistic SOC to train on, test detections against, or run as a homelab, but assembling six tools by hand and integrating them eats days. It differs from a pile of per-tool guides by treating the whole stack as one declarative, idempotent, agent-friendly deploy: each tool is a self-contained LXC component, cross-component integrations wire automatically, and the entire run is non-interactive with JSON output so an AI agent can SSH in and one-shot it.
Website: lidless.dev/soc-stack
Project status. v1.0.0 is tagged and the full stack deploys and asserts green end-to-end on Proxmox VE 7.x/8.x/9.x. It is an actively developed, single-maintainer lab tool built for homelabs, training, and internal SOC replication, not a hardened multi-tenant production deployment. See SECURITY.md for the threat model and the CHANGELOG for what is in flight.
SOC Stack is a self-hosted SOC lab builder for homelabs and security training. Run one command on a Proxmox VE host and roughly 30 minutes later you have a working Security Operations Center:
- Wazuh for SIEM / XDR (alerting, FIM, vulnerability detection, agent management)
- TheHive + Cortex for case management and SOAR (analyzers, responders, observable enrichment)
- MISP for threat intelligence (IOC sharing, feeds, correlation)
- Zeek + Suricata for network security monitoring and intrusion detection (NSM + IDS/IPS)
- Custom dashboards (Bro Hunter + Playbook Forge) behind nginx
- 9 MCP servers so an AI agent can query Wazuh, TheHive, Cortex, MISP, Zeek, Suricata, MITRE ATT&CK, Rapid7, and Sophos over a single MCP config
Every tool runs in its own dedicated, unprivileged LXC. The orchestrator handles VMID allocation, network setup, idempotency, secret generation, and cross-component integration wiring. The whole run is non-interactive by default and emits structured JSON, so a person or an agent can replicate the same lab on demand.
Keywords: SOC lab, security operations center, Proxmox homelab, Wazuh SIEM, TheHive, Cortex SOAR, MISP threat intelligence, Suricata IDS, Zeek NSM, blue team training, detection engineering, self-hosted security, MCP servers for security tooling.
Full stack (every component, sensible defaults):
curl -sSL https://raw.githubusercontent.com/solomonneas/soc-stack/main/install.sh | sudo bashWhen run from a local TTY as sudo bash install.sh, the installer opens a component picker if you did not pass --components or --manifest. Piped, CI, and agent-driven installs remain non-interactive and default to the full stack.
Custom subset:
curl -sSL https://raw.githubusercontent.com/solomonneas/soc-stack/main/install.sh | sudo bash -s -- \
--components wazuh,thehive-cortex,misp \
--preset standard \
--bridge vmbr0 --storage local-lvmAgent-driven (fully non-interactive, structured output):
curl -sSL https://raw.githubusercontent.com/solomonneas/soc-stack/main/install.sh | sudo bash -s -- \
--components all \
--preset minimal \
--bridge vmbr0 --storage local-lvm --ip-mode dhcp \
--json-out /root/soc-stack.json \
--mcp-config-out /root/mcp-clients.jsonPrefer to read before you run? Clone the repo and execute the same orchestrator locally; the behavior is identical:
git clone https://github.com/solomonneas/soc-stack.git
cd soc-stack
sudo bash install.sh --components all --dry-run # validate + plan, deploy nothingAfter install:
/root/soc-stack.jsonlists every component with its LXC VMID, IP, ports, endpoints, warnings, and secret file paths. Raw passwords and API tokens are redacted by default; pass--include-secrets-jsononly when an automation workflow explicitly needs them./root/mcp-clients.jsonis a paste-readymcpServersconfig block for Claude Desktop, OpenClaw, or any MCP client. It contains bearer tokens and is written root-only./var/lib/soc-stack/state/has per-component state files used for idempotent re-runs./var/lib/soc-stack/secrets/has every generated credential (mode 0600, root-only) for audit recovery.
Re-run the same command with --force to redeploy a completed component, or with --components <one> to add a single component to an existing install.
| Component | Services | LXC preset (minimal) | Ports |
|---|---|---|---|
| wazuh | Wazuh Manager, Indexer, Dashboard | 2 vCPU, 2 GB RAM, 30 GB | 443, 1514, 1515, 55000 |
| thehive-cortex | TheHive 5.4, Cortex 3.1.8, Elasticsearch 7.17, Cassandra 4.1 | 2 vCPU, 4 GB RAM, 30 GB | 9000, 9001 |
| misp | MISP, MariaDB 10.11, Redis 7, misp-modules | 1 vCPU, 2 GB RAM, 20 GB | 443 |
| zeek-suricata | Zeek (NSM), Suricata (IDS/IPS) | 1 vCPU, 2 GB RAM, 20 GB | 47760 |
| dashboards | Bro Hunter + Playbook Forge behind nginx | 1 vCPU, 1 GB RAM, 10 GB | 80, 5174, 5177 |
| mcp | 9 MCP servers (wazuh, thehive, cortex, misp, zeek, suricata, mitre, rapid7, sophos) wrapped as SSE via mcp-proxy |
1 vCPU, 1 GB RAM, 10 GB | 3001-3009 |
Each component runs in its own dedicated LXC. Components can be deployed independently or together. The orchestrator handles VMID allocation, network setup, idempotency, and cross-component integration wiring.
Configured automatically after all components deploy:
- Wazuh -> TheHive: Wazuh alerts at level 8+ forward to TheHive as alerts via a custom Python integration (
/var/ossec/integrations/custom-thehive.py). - TheHive <-> Cortex: TheHive's Cortex connector points at the local Cortex with an org-scoped API key.
- MISP -> Suricata: hourly cron pulls Snort/Suricata rules from MISP's
restSearchendpoint into Suricata's update.d. - Zeek -> Wazuh: Wazuh agent runs in the zeek-suricata LXC and forwards conn.log, dns.log, http.log, ssl.log, notice.log to the Wazuh manager.
- MCP servers <- all peers: each MCP server's env file is populated with its corresponding tool's URL + API key from peer state.
/root/soc-stack.json is the source of truth for what got deployed. With secrets redacted, a full-stack run reports each component, its LXC, its endpoints, and any warnings (IPs and tokens below are placeholder values from the RFC 5737 documentation range):
{
"version": "1.0.0",
"preset": "minimal",
"status": "deployed",
"components": [
{
"component": "wazuh",
"status": "deployed",
"vmid": 9001,
"ip": "192.0.2.11",
"endpoints": { "dashboard": "https://192.0.2.11:443", "api": "https://192.0.2.11:55000" },
"credentials": { "user": "admin", "password": "REDACTED" }
},
{
"component": "mcp",
"status": "deployed",
"vmid": 9006,
"ip": "192.0.2.16",
"endpoints": { "wazuh_sse": "http://127.0.0.1:3001/sse", "thehive_sse": "http://127.0.0.1:3002/sse" }
}
],
"integrations": [
{ "from": "wazuh", "to": "thehive", "status": "wired" },
{ "from": "misp", "to": "suricata", "status": "wired" }
]
}The exact result-JSON schema is documented in docs/design/specs/2026-05-15-soc-stack-unification-design.md.
Designed so an AI agent can SSH into a Proxmox host and one-shot a SOC. The full agent surface:
- Stdin is closed under
curl | sudo bash; the installer auto-detects this and enables--non-interactivemode. Every prompt becomes a flag, every default becomes an answer. - Exit codes are stable: 0 = success, 1 = preflight (bad host), 2 = validation (bad flags), 3 = component failed, 4 = integration failed, 5 = mixed state.
- Result JSON schema is documented in
docs/design/specs/2026-05-15-soc-stack-unification-design.md. - Idempotency: re-running with the same flags exits in seconds if everything is already deployed (
status: "deployed"in state).--forcetriggers redeploy. - Manifest mode: instead of dozens of flags, write a JSON manifest and pass
--manifest <path>. CLI flags applied on top override individual manifest fields.
--components LIST CSV of components or "all" (default: all)
--preset NAME minimal | standard | production (default: standard)
--bridge NAME Proxmox bridge (default: vmbr0)
--storage NAME Storage pool (default: auto-detect)
--ip-mode MODE dhcp or static (default: dhcp)
--ip-range CIDR Required if --ip-mode=static (e.g., 198.51.100.10/24)
--vlan TAG Optional VLAN tag
--vmid-start N First VMID to allocate (default: next free)
--manifest PATH JSON manifest (alternative to flags)
--state-dir PATH State directory (default: /var/lib/soc-stack)
--json-out PATH Result JSON path (default: /root/soc-stack.json)
--mcp-config-out PATH MCP client config (default: /root/mcp-clients.json)
--log-file PATH Install log (default: /var/log/soc-stack-install.log)
--dry-run Validate + plan only, no deploy
--force Redeploy components already marked deployed
--no-integrate Skip cross-component wiring phase
--non-interactive Hard-fail on prompts (auto when stdin is not a TTY)
--include-secrets-json
Include raw credentials in result JSON (default: redacted)
--mcp-bind-host HOST MCP SSE bind host (default: 127.0.0.1; use 0.0.0.0 to expose)
--version Print version and exit
soc-stack/
├── install.sh # repo-root wrapper for curl|bash
├── scripts/
│ ├── install.sh # orchestrator (~430 lines)
│ ├── lib/ # 8 shared bash modules (bats-tested)
│ │ ├── logging.sh
│ │ ├── secrets.sh
│ │ ├── json-out.sh
│ │ ├── idempotency.sh
│ │ ├── network.sh
│ │ ├── manifest.sh
│ │ ├── preflight.sh
│ │ └── lxc.sh
│ └── components/
│ ├── wazuh/ # manifest.jsonc + 5 scripts per component
│ ├── thehive-cortex/
│ ├── misp/
│ ├── zeek-suricata/
│ ├── dashboards/
│ └── mcp/ # 9 MCP servers + mcp-proxy SSE bridge
├── tests/
│ ├── unit/ # 105 bats tests, mocked Proxmox binaries
│ └── integration/ # per-component + cross-component assertions
├── docs/
│ ├── design/specs/ # design spec (result JSON schema lives here)
│ ├── gotchas.md
│ ├── adding-a-component.md # component contract walk-through
│ └── architecture/
├── playbooks/ # incident response playbooks
├── cases/ # case study evidence
└── mcp-servers/
└── README.md # docs for the 9 bundled MCP servers
Each component is a self-contained folder under scripts/components/<name>/ with a fixed interface:
| File | Runs where | Purpose |
|---|---|---|
manifest.jsonc |
(declarative) | Presets, ports, deps, provides |
lxc-spec.sh |
Proxmox host | Emits pct create flags per preset |
deploy.sh |
inside LXC | Idempotent installer; writes state JSON |
verify.sh |
inside LXC | Health check; exit 0 if healthy |
integrate.sh |
Proxmox host | Wires this component to peers (reads peer state) |
destroy.sh |
Proxmox host | Tears down the LXC + state |
The orchestrator (scripts/install.sh) only talks to components through this interface. Adding a new component means dropping in a new folder; nothing else changes.
State files in /var/lib/soc-stack/state/<name>.json are the source of truth for idempotency. Re-running install.sh checks each component's state and skips anything already deployed (unless --force). On failure, the state file records status: "failed" and an error string; the orchestrator continues with remaining independent components and reports mixed-state exit code 5.
- Proxmox VE 7.x or 8.x or 9.x host
- Root access on the Proxmox host
- A bridge (default:
vmbr0) and a storage pool (default: auto-detect, falls back tolocal-lvm) - Outbound HTTPS for installer downloads (Docker, Wazuh installer, MCP server repos, etc.)
- ~12 GB free RAM and ~150 GB free disk for the full stack at
--preset minimal
The installer auto-installs jq, curl, wget, and openssl if missing.
Re-run for a single component:
sudo bash install.sh --components misp --forceRe-run the integration phase (after fixing a peer):
sudo bash install.sh --components allAlready-deployed components are skipped by the idempotency check, so a plain re-run goes straight to cross-component wiring.
Validate without deploying:
sudo bash install.sh --components all --dry-runRemove a single component:
sudo bash scripts/components/misp/destroy.shThis stops and destroys the component's LXC and removes its state file. Other components keep running; re-run the installer afterwards if peers should drop their wiring to it.
Tear down everything:
for comp in mcp dashboards zeek-suricata misp thehive-cortex wazuh; do
sudo bash scripts/components/${comp}/destroy.sh
done
sudo rm -rf /var/lib/soc-stack /root/soc-stack.json /root/mcp-clients.jsonThe final rm removes state, generated secrets, and the emitted JSON; skip it if you want credential recovery later.
Upgrade:
curl -sSL https://raw.githubusercontent.com/solomonneas/soc-stack/main/install.sh | sudo bashRe-running the installer from a newer checkout is the upgrade path: already-deployed components are left alone, new components deploy, and integration re-wires. To pick up a new version of one component, destroy it and re-run with --components <name>. The installer never auto-updates a running component in place.
- Why not install each tool by hand? You can, and the official docs for Wazuh, TheHive, MISP, and Suricata are good. But six installs plus the integrations between them (alert forwarding, analyzer wiring, IOC feeds, log shipping) is a multi-day project that breaks the next time you rebuild. SOC Stack makes the whole thing one reproducible command.
- Why not a single all-in-one SIEM VM (SecurityOnion, Wazuh OVA, etc.)? Those are excellent and purpose-built. SOC Stack is different on purpose: each tool lives in its own LXC you can scale, snapshot, or destroy independently, the components are the real upstream projects (not a fork), and the cross-tool integrations are explicit and inspectable rather than baked into one appliance.
- Why not Ansible or Terraform? Nothing stops you, and a config-management rewrite is a reasonable future direction. The current design favors plain, auditable bash you can read top to bottom and a
curl | sudo bashpath an agent can drive without extra tooling on the host. State files, not a state backend, drive idempotency. - Why not run it on Docker / Kubernetes directly? Several components already use Docker Compose inside their LXC. The Proxmox LXC layer gives each tool isolation, its own IP, and snapshot/rollback at the container level, which matches how a homelab SOC is actually operated.
- Not a hardened production SOC. It is a lab and training tool. It assumes a trusted Proxmox host and a trusted internal bridge. Do not expose the component IPs to an untrusted network without a firewall, VLAN, and TLS termination in front. The full threat model is in SECURITY.md.
- Not a managed or hosted service. There is no SaaS, no telemetry, and no phone-home. Everything runs on hardware you control.
- Not a fork or a repackage of the upstream tools. It deploys Wazuh, TheHive, Cortex, MISP, Zeek, and Suricata from their real sources at pinned versions; it does not modify them.
- Not an auto-updater. The installer pins versions and never silently upgrades a running component in place; updates happen on your schedule.
- Not multi-host. It targets a single Proxmox host. Multi-node is out of scope today.
See docs/adding-a-component.md for the component contract walk-through, and docs/design/specs/2026-05-15-soc-stack-unification-design.md for the full design.
Contributions are welcome. New components follow a six-file contract, every lib function gets a bats test, and CI runs shellcheck plus bats on every PR. Start with CONTRIBUTING.md and the Code of Conduct.
Default credentials are rotated and verified on deploy, secrets are root-only, result JSON is redacted by default, and MCP servers bind to localhost unless you say otherwise. The full threat model, what is hardened versus deliberately accepted, lives in SECURITY.md. Found a vulnerability? Use GitHub's private vulnerability reporting on this repository.
MIT. Copyright (c) 2026 Solomon Neas.
