Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsTraining
StudyProjectsNewsletterHire MeAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Training
Study
Projects
Newsletter
Hire Me
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

1481+ Articles
152+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Checklists
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. Building a SOAR Platform with Shuffle in Your Homelab
Building a SOAR Platform with Shuffle in Your Homelab
PROJECTIntermediate

Building a SOAR Platform with Shuffle in Your Homelab

Deploy Shuffle, the open-source SOAR platform, to automate security workflows and orchestrate your homelab tools — from Wazuh alerts to enrichment lookups and automated responses.

Dylan H.

Projects

June 17, 2026
11 min read
3-5 hours

Tools & Technologies

DockerDocker ComposeGitcurl

Overview

Security Orchestration, Automation and Response (SOAR) platforms are the connective tissue of a mature security operations workflow. Instead of analysts manually pivoting between tools when an alert fires, a SOAR platform receives the alert, enriches it with threat intelligence, creates a ticket, notifies the team, and can even trigger a containment action — all automatically.

Shuffle is an open-source SOAR platform built for flexibility. It ships as a self-hosted Docker stack, comes with hundreds of pre-built app integrations (Wazuh, TheHive, MISP, Slack, VirusTotal, and more), and uses a drag-and-drop workflow editor. It is a legitimate alternative to commercial platforms like Splunk SOAR and Palo Alto XSOAR for homelab and small-team environments.

In this guide you will stand up a full Shuffle deployment, connect it to Wazuh for real alert input, and build two practical workflows: an automated alert triage workflow and a VirusTotal hash enrichment workflow.


Architecture

The Shuffle stack is made up of four services:

ServiceImageRole
shuffle-frontendghcr.io/shuffle/shuffle-frontend:latestReact UI on port 3001
shuffle-backendghcr.io/shuffle/shuffle-backend:latestGo REST API on port 5001
shuffle-orborusghcr.io/shuffle/shuffle-orborus:latestWorkflow execution engine — spawns worker containers via Docker
opensearchopensearchproject/opensearch:3.2.0Persistent data store (indices, workflow state, execution logs)

Orborus is the key piece: when a workflow is triggered, Orborus pulls the workflow definition from the backend and spawns isolated Docker containers for each action node. This means every app runs in its own container with its own credentials — no cross-contamination.

External Alert Source (Wazuh)
        │
        ▼  HTTP POST (JSON)
   Shuffle Webhook ──► Backend API ──► Orborus
                                          │
                                          ├─► Worker: Enrich (VirusTotal)
                                          ├─► Worker: Create Ticket (TheHive)
                                          └─► Worker: Notify (Slack / Email)

Prerequisites

  • Linux host with Docker Engine and Docker Compose v2 installed
  • Minimum 4 GB RAM (OpenSearch alone uses ~3 GB heap)
  • Ports 3001, 5001, and 9200 available on the host
  • Git installed

Step 1: Prepare the Host

OpenSearch requires a higher virtual memory limit than the Linux kernel default. Set this before launching any containers or the OpenSearch container will fail to start.

# Apply immediately (no reboot required)
sudo sysctl -w vm.max_map_count=262144
 
# Make permanent across reboots
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf

Disable swap — OpenSearch degrades severely when swapping and recommends it be off entirely:

sudo swapoff -a

Clone the official Shuffle repository to get the canonical docker-compose.yml and .env template:

git clone https://github.com/Shuffle/Shuffle
cd Shuffle

Fix the database directory ownership before first launch:

sudo chown -R 1000:1000 shuffle-database

Step 2: Configure the Environment

Open .env in an editor. The following variables must be set before starting:

# .env
 
# ----- Authentication -----
SHUFFLE_DEFAULT_USERNAME=admin
SHUFFLE_DEFAULT_PASSWORD=ChangeMeNow321!
 
# Encryption key — set any random string, keep it secret
SHUFFLE_ENCRYPTION_MODIFIER=ChangeThisToSomethingRandom
 
# ----- OpenSearch credentials -----
SHUFFLE_OPENSEARCH_URL=https://localhost:9200
SHUFFLE_OPENSEARCH_USERNAME=admin
SHUFFLE_OPENSEARCH_PASSWORD=StrongShufflePassword321!
SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY=true
SHUFFLE_ELASTIC=true
 
# ----- Network -----
# Change OUTER_HOSTNAME to your LAN IP if accessing from another machine
OUTER_HOSTNAME=localhost
FRONTEND_PORT=3001
FRONTEND_PORT_HTTPS=3443
BACKEND_PORT=5001
 
# ----- Execution -----
SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY=5
SHUFFLE_CONTAINER_AUTO_CLEANUP=true
AUTH_FOR_ORBORUS=true

Security note: Change all default passwords before deploying. The SHUFFLE_OPENSEARCH_PASSWORD must be identical in both the .env and inside the OpenSearch service's OPENSEARCH_INITIAL_ADMIN_PASSWORD environment variable — these are set in docker-compose.yml.


Step 3: Review the Docker Compose Configuration

The docker-compose.yml from the repository is production-ready. Key sections to understand:

services:
  frontend:
    image: ghcr.io/shuffle/shuffle-frontend:latest
    ports:
      - "${FRONTEND_PORT}:80"
      - "${FRONTEND_PORT_HTTPS}:443"
    environment:
      - BACKEND_HOSTNAME=${OUTER_HOSTNAME}
 
  backend:
    image: ghcr.io/shuffle/shuffle-backend:latest
    ports:
      - "${BACKEND_PORT}:5001"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock  # Required for Orborus to spawn workers
      - ./shuffle-files:/shuffle-files
    environment:
      - SHUFFLE_OPENSEARCH_URL=${SHUFFLE_OPENSEARCH_URL}
      - SHUFFLE_ENCRYPTION_MODIFIER=${SHUFFLE_ENCRYPTION_MODIFIER}
 
  orborus:
    image: ghcr.io/shuffle/shuffle-orborus:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock  # Orborus needs this to spawn worker containers
    environment:
      - BASE_URL=http://shuffle-backend:5001
      - SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY=${SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY}
 
  opensearch:
    image: opensearchproject/opensearch:3.2.0
    environment:
      - OPENSEARCH_JAVA_OPTS=-Xms3072m -Xmx3072m
      - cluster.name=shuffle-cluster
    volumes:
      - shuffle-database:/usr/share/opensearch/data
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536

The Docker socket mount on both backend and orborus is intentional and required — Orborus cannot spawn worker containers without it. Be aware this gives those containers root-equivalent access to the Docker daemon.


Step 4: Start the Stack

docker compose up -d

Watch the logs for startup completion. OpenSearch takes 60–90 seconds on first boot while it initialises indices:

docker compose logs -f opensearch
# Wait for: [cluster_manager] Cluster health status changed from [RED] to [GREEN]
 
docker compose logs -f backend
# Wait for: Successfully connected to opensearch

Verify all services are healthy:

docker compose ps

All four containers should show running. If OpenSearch shows exited, the most common cause is the vm.max_map_count not being applied — re-run the sysctl command from Step 1 and restart the container.


Step 5: Initial Web Setup

Navigate to http://localhost:3001 (or your OUTER_HOSTNAME:3001 from another machine).

You will see the Shuffle registration screen. Create your admin account — use credentials you actually want to keep. The SHUFFLE_DEFAULT_USERNAME/SHUFFLE_DEFAULT_PASSWORD variables in .env do not auto-provision an account; you must create it through the UI on first visit.

After logging in:

  1. Click Apps in the top menu bar
  2. Click Download apps — this pulls the Shuffle community app library from GitHub (~500 integrations)
  3. Wait for the sync to complete — you will see app icons populate

Step 6: Build a Webhook Trigger Workflow

This is the foundation for all external integrations. Every tool (Wazuh, Grafana, custom scripts) will send data into Shuffle via a webhook.

  1. Navigate to Automate → Workflows
  2. Click + New workflow
  3. Name it Alert Triage, leave the default trigger type, click Create
  4. In the workflow editor, locate the Triggers section in the left panel
  5. Drag a Webhook trigger node onto the canvas
  6. Click the webhook node — note the Webhook URI (format: https://<HOST>:3001/api/v1/hooks/webhook_<UUID>) — copy this URL
  7. Click Start node to activate the trigger

The webhook is now live. You can test it immediately:

curl -XPOST https://localhost:3001/api/v1/hooks/webhook_<YOUR-UUID> \
  -H "Content-Type: application/json" \
  -d '{"alert_id": "test-001", "severity": "high", "message": "Test alert from curl"}'

In the Shuffle UI, click the Executions icon on the webhook node — you should see the test execution with your JSON payload.


Step 7: Integrate Wazuh Alerts

With the webhook URL in hand, configure Wazuh to forward alerts to Shuffle.

On your Wazuh manager, edit /var/ossec/etc/ossec.conf and add the integration block inside the <ossec_config> root element:

<integration>
  <name>shuffle</name>
  <hook_url>https://<SHUFFLE_IP>:3001/api/v1/hooks/webhook_<YOUR-UUID></hook_url>
  <level>5</level>
  <alert_format>json</alert_format>
</integration>
  • <level>5</level> — only forward alerts at severity level 5 or above. Adjust this threshold based on your alert volume. Level 3 is very noisy; level 7 or above limits to high-severity events only.
  • <alert_format>json</alert_format> is required — Shuffle expects structured JSON.

Restart the Wazuh manager to apply:

sudo systemctl restart wazuh-manager

Trigger a test rule (e.g., a failed SSH login) and verify that the execution appears in your Shuffle workflow's execution history.


Step 8: Add Enrichment with VirusTotal

Extend the triage workflow to look up file hashes against VirusTotal automatically.

  1. In your Alert Triage workflow, open the Apps panel on the left
  2. Search for VirusTotal — drag the app onto the canvas
  3. Connect the webhook node's output arrow to the VirusTotal node
  4. Click the VirusTotal node and select the Get hash report action
  5. In the API Key field, paste your VirusTotal API key (free tier gives 4 requests/minute — sufficient for homelab)
  6. In the Hash field, use the variable picker to reference the hash from the incoming alert: $exec.body.data.win.eventdata.hashes

Wazuh alert field paths vary by rule. Use the execution viewer to inspect the exact JSON structure of a real alert before wiring up variables.

  1. Add a Repeat back to me (Echo) node after VirusTotal to log the enrichment result during testing
  2. Click Save and trigger a new test alert

The VirusTotal node will return a full threat intelligence report including detection count, last analysis date, and reputation score — all logged in the execution history.


Step 9: Add Slack/Email Notification

Keep the workflow linear: webhook → VirusTotal → Notify.

For Slack:

  1. Add a Slack app node after the VirusTotal step
  2. Select the Send message action
  3. Configure an Incoming Webhook in your Slack workspace and paste the URL into the app authentication
  4. In the message body, reference enrichment data:
    :rotating_light: New alert: $exec.body.rule.description
    Severity: $exec.body.rule.level
    VirusTotal detections: $virustotal_step.body.data.attributes.last_analysis_stats.malicious
    

For email (using the Email app):

  1. Add an Email node after the VirusTotal step
  2. Configure SMTP credentials in the app authentication
  3. Set recipient, subject, and body with the same variable substitution syntax

Step 10: Reverse Proxy with Nginx (Optional but Recommended)

Running Shuffle directly on port 3001 with a self-signed cert is fine for a homelab, but adding Nginx gives you proper TLS termination and a clean domain name.

Create /etc/nginx/sites-available/shuffle:

server {
    listen 80;
    server_name shuffle.yourdomain.local;
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl;
    server_name shuffle.yourdomain.local;
 
    ssl_certificate     /etc/ssl/certs/shuffle.crt;
    ssl_certificate_key /etc/ssl/private/shuffle.key;
 
    location / {
        proxy_pass         http://localhost:3001;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_read_timeout 300s;
        proxy_buffering    off;
    }
}

Enable and reload:

sudo ln -s /etc/nginx/sites-available/shuffle /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Update OUTER_HOSTNAME in your .env to your domain name and restart the frontend container:

docker compose restart frontend

Testing

End-to-end test checklist:

# 1. Confirm all containers are running
docker compose ps
 
# 2. Trigger a manual webhook delivery
curl -XPOST http://localhost:3001/api/v1/hooks/webhook_<UUID> \
  -H "Content-Type: application/json" \
  -d '{"rule": {"level": 7, "description": "Brute force attack"}, "agent": {"name": "test-host"}}'
 
# 3. Check the execution result in the Shuffle UI
# Navigate to Automate → Workflows → Alert Triage → Executions
 
# 4. Verify Wazuh forwarding is working
sudo tail -f /var/ossec/logs/integrations.log
# Look for: shuffle: alert forwarded
 
# 5. Check Orborus can spawn workers
docker ps | grep shuffle
# Worker containers should appear briefly during execution

If executions hang at a node, check the Orborus logs for Docker pull errors — the first run of each app pulls its worker image, which can take a minute on slower connections:

docker compose logs orborus --tail 50

Common Troubleshooting

SymptomLikely CauseFix
OpenSearch exits immediatelyvm.max_map_count too lowsudo sysctl -w vm.max_map_count=262144
Backend can't connect to OpenSearchWrong password in .envMatch SHUFFLE_OPENSEARCH_PASSWORD across both places
Workflow execution stuckOrborus can't pull app imageCheck internet access from host; see docker compose logs orborus
Wazuh not forwardingIntegration config missing or wrong URLVerify /var/ossec/logs/integrations.log; check <alert_format>json</alert_format> is set
Webhook triggers but nodes don't fireWorkflow not saved or node not connectedClick Save after every workflow change

Extensions and Next Steps

With the core platform running, these integrations add significant capability:

TheHive case management — Connect Shuffle to TheHive (also Docker-deployable) so that every confirmed high-severity alert automatically creates an investigation case with all enrichment data pre-populated. The TheHive app ships with Shuffle out of the box.

MISP threat intelligence — Add a MISP node to your triage workflow to check incoming IOCs (IPs, hashes, domains) against your local threat intelligence database before deciding on response actions.

Automated containment — Connect the Wazuh app in Shuffle (not just the webhook integration). The Wazuh app exposes an Active Response action that lets you block IPs or kill processes directly from a workflow — closing the loop from detection to response without analyst intervention.

Scheduled workflows — Shuffle supports Schedule triggers in addition to webhooks. Use these for daily tasks: pulling new threat intelligence into MISP, generating weekly alert summary reports, or sweeping for newly exposed services.

Multi-tenancy — Shuffle supports organizations, allowing you to partition workflows and apps across multiple teams or environments within a single deployment.

Backup strategy — The OpenSearch volume (shuffle-database) contains all workflow definitions, execution history, and credentials. Add a cron job to snapshot this volume daily:

# /etc/cron.d/shuffle-backup
0 3 * * * root docker run --rm \
  -v shuffle_shuffle-database:/data \
  -v /backups/shuffle:/backup \
  busybox tar czf /backup/shuffle-$(date +%F).tar.gz /data

Shuffle turns a collection of point solutions (SIEM, ticketing, threat intel, notifications) into a coordinated security operations workflow. Once the core automation is running, the time-to-response for common alert types drops from minutes to seconds — and your analysts spend that saved time on investigations that actually require human judgement.

#soar#shuffle#security-automation#incident-response#docker#homelab#wazuh#open-source

Related Articles

Teleport PAM: Zero-Trust Privileged Access for Your Homelab

Deploy Teleport's open-source privileged access management platform to replace static SSH keys with short-lived certificates, enforce MFA, record every...

11 min read

Building a Wazuh XDR + SIEM Homelab

Deploy a full Wazuh stack in Docker to gain host-based intrusion detection, file integrity monitoring, vulnerability scanning, and active response across your…

14 min read

Runtime Security Monitoring with Falco: Detect Container

Deploy Falco on a Docker host to monitor container syscalls at the kernel level, write custom homelab detection rules, and route real-time alerts through.

12 min read
Back to all Projects