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:
| Service | Image | Role |
|---|---|---|
| shuffle-frontend | ghcr.io/shuffle/shuffle-frontend:latest | React UI on port 3001 |
| shuffle-backend | ghcr.io/shuffle/shuffle-backend:latest | Go REST API on port 5001 |
| shuffle-orborus | ghcr.io/shuffle/shuffle-orborus:latest | Workflow execution engine — spawns worker containers via Docker |
| opensearch | opensearchproject/opensearch:3.2.0 | Persistent 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.confDisable swap — OpenSearch degrades severely when swapping and recommends it be off entirely:
sudo swapoff -aClone the official Shuffle repository to get the canonical docker-compose.yml and .env template:
git clone https://github.com/Shuffle/Shuffle
cd ShuffleFix the database directory ownership before first launch:
sudo chown -R 1000:1000 shuffle-databaseStep 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=trueSecurity note: Change all default passwords before deploying. The
SHUFFLE_OPENSEARCH_PASSWORDmust be identical in both the.envand inside the OpenSearch service'sOPENSEARCH_INITIAL_ADMIN_PASSWORDenvironment variable — these are set indocker-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: 65536The 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 -dWatch 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 opensearchVerify all services are healthy:
docker compose psAll 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:
- Click Apps in the top menu bar
- Click Download apps — this pulls the Shuffle community app library from GitHub (~500 integrations)
- 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.
- Navigate to Automate → Workflows
- Click + New workflow
- Name it
Alert Triage, leave the default trigger type, click Create - In the workflow editor, locate the Triggers section in the left panel
- Drag a Webhook trigger node onto the canvas
- Click the webhook node — note the Webhook URI (format:
https://<HOST>:3001/api/v1/hooks/webhook_<UUID>) — copy this URL - 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-managerTrigger 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.
- In your
Alert Triageworkflow, open the Apps panel on the left - Search for VirusTotal — drag the app onto the canvas
- Connect the webhook node's output arrow to the VirusTotal node
- Click the VirusTotal node and select the Get hash report action
- In the API Key field, paste your VirusTotal API key (free tier gives 4 requests/minute — sufficient for homelab)
- 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.
- Add a Repeat back to me (Echo) node after VirusTotal to log the enrichment result during testing
- 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:
- Add a Slack app node after the VirusTotal step
- Select the Send message action
- Configure an Incoming Webhook in your Slack workspace and paste the URL into the app authentication
- 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):
- Add an Email node after the VirusTotal step
- Configure SMTP credentials in the app authentication
- 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 nginxUpdate OUTER_HOSTNAME in your .env to your domain name and restart the frontend container:
docker compose restart frontendTesting
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 executionIf 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 50Common Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| OpenSearch exits immediately | vm.max_map_count too low | sudo sysctl -w vm.max_map_count=262144 |
| Backend can't connect to OpenSearch | Wrong password in .env | Match SHUFFLE_OPENSEARCH_PASSWORD across both places |
| Workflow execution stuck | Orborus can't pull app image | Check internet access from host; see docker compose logs orborus |
| Wazuh not forwarding | Integration config missing or wrong URL | Verify /var/ossec/logs/integrations.log; check <alert_format>json</alert_format> is set |
| Webhook triggers but nodes don't fire | Workflow not saved or node not connected | Click 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 /dataShuffle 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.